• 基于 Socket 的网络编程


    网络编程

    指网络上的主机, 通过不同的进程, 以编程的方式实现 网络通信 (或称为网络数据传输) (同一台主机的不同进程间, 基于网路的通信也可以称为网络编程)


    服务端 & 客户端

    网络编程中的概念
    服务端: 网络通信中, 提供服务的一方 (进程)
    客户端: 网络通信中, 获取服务的一方 (进程)


    Socket 套接字

    Socket 套接字, 是由系统提供的, 用于网络通信的技术, 是基于 TCP/IP 协议的, 网络通信的基本操作单元. 基于 Socket 套接字的网络程序开发就是网络编程


    Socket 的分类

    Socket 套接字根据 针对的传输层协议 可以分为三类:

    1. 流套接字 – 使用传输层 TCP 协议
    2. 数据报套接字 – 使用传输层 UDP 协议
    3. 原始套接字 – 使用自定义传输层协议

    TCP & UDP

    TCP (Transmission Control Protocol) 传输控制协议 (传输层协议), 特点:

    • 有连接
    • 可靠传输
    • 面向字节流
    • 全双工
    • 有发送缓冲区, 也有接收缓冲区
    • 大小不限 (基于 IO 流)

    UDP (User Datagram Protocol) 用户数据报协议 (传输层协议), 特点:

    • 无连接
    • 不可靠传输
    • 面向数据报
    • 全双工
    • 有接收缓冲区, 无发送缓冲区
    • 大小受限, 一次最多 64K

    TCP 中的长短连接

    短连接: 每次接收到数据并返回响应后, 都会关闭连接. (短连接只能一次收发数据)
    长连接: 不关闭连接, 一直保持连接状态, 双方不停的收发数据. (长连接可以多次收发数据)

    长短连接拥有不同的特点 :

    • 建立连接, 关闭连接都需要消耗资源, 因此长连接效率更高
      • 短连接一般是客户端主动向服务端发送请求.
      • 而长连接可以是客户端主动向服务端发送请求, 也可以是服务端主动向客户端推送消息
      • 短连接适用于客户端请求频率不高的场景, eg: 浏览网页.
      • 长连接适用于客户端和服务器通信频繁的场景, eg: 聊天室, 实时游戏

    BIO & NIO

    长连接有两种实现方式, 基于 BIO 的长连接和基于 NIO 的长连接

    • BIO (同步阻塞 IO) : 基于 BIO 的长连接会一直占用系统资源. 在并发情况下, 每个连接都需要阻塞等待, 接收数据. 即每个连接在一个线程中运行, 消耗极大.
    • NIO (非同步阻塞 IO) : 长连接常用实现, 可以极大的提高性能.

    封装/分用 vs 序列化/反序列化

    对于 发送数据 时的 数据包装动作 来说

    • 如果使用 知名协议, 这个动作称为 封装
    • 如果使用 小众协议 / 自定义协议, 这个动作称为 序列化 (一般是将对象转换成特定数据格式)

    对于 接收数据 时的 数解析动作 来说

    • 如果使用 知名协议, 这个动作称为 分用
    • 如果使用 小众协议 / 自定义协议, 这个动作称为 反序列化 (一般是基于接收数据特定的格式, 转换成程序中的对象 )

    协议的设计

    一般根据字段的特点进行设计
    在这里插入图片描述
    除此之外, 协议中还会包含: 状态码, 请求类型 等等内容


    基于 TCP 的回显服务器设计

    服务端

    1. 使用 ServerScoket 创建服务端程序
    2. 调用 ServerSocket.accept() 方法建立连接
    3. 读取请求数据
    4. 根据请求计算响应 (此处为回显服务器, 将请求数据当作响应数据直接返回)
    5. 返回响应数据
    package network;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.nio.charset.StandardCharsets;
    import java.util.Scanner;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class TcpEchoServer {
        private ServerSocket serverSocket = null;
    
        public TcpEchoServer(int port) throws IOException {
            serverSocket = new ServerSocket(port);
        }
    
        public void start() throws IOException {
            System.out.println("启动服务器");
            // 此处使用 CachedThreadPool, 使用 FixedThreadPool 不太合适 (线程数量不应该固定)
            ExecutorService threadPool = Executors.newCachedThreadPool();
            while(true) {
                Socket clientSocket = serverSocket.accept();
                threadPool.submit(() -> {
                    processConnection(clientSocket);
                });
            }
        }
    
        // 使用该方法来处理一个连接
        // 这一个连接对应一个客户端 (一次连接可能会涉及到多次交互)
        private void processConnection(Socket clientSocket) {
            System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
    
            // 基于上述 socket 对象和客户端进行通讯
            try(InputStream inputStream = clientSocket.getInputStream();
                OutputStream outputStream = clientSocket.getOutputStream()) {
                // 由于要处理多个请求和响应, 因此使用循环来进行
                while(true) {
                    // 1. 读取请求
                    Scanner sc = new Scanner(inputStream);
                    if(!sc.hasNext()) {
                        // 没有下个数据, 说明读完了. (客户端关闭了连接)
                        System.out.printf("[%s:%d] 客户端下线!\n",
                                clientSocket.getInetAddress(),
                                clientSocket.getPort());
                        break;
                    }
                    // 注意, 此处使用 next 是一直读取到 换行符/空格/其他空白符 结束, 但是最终结果不包含上述 空白符(因此后面要自己再补一个空白符).
                    String request = sc.next();
                    // 根据请求构造响应
                    String response = process(request);
                    // 3.返回响应结果.
                    outputStream.write((response+"\n").getBytes(StandardCharsets.UTF_8));
                    outputStream.flush();
                    //   OutputString 没有 write(String) 这样的功能. 可以把 String 里的字节数组拿出来, 进行写入;
                    //   也可以使用 字符流 来转换
    //                PrintWriter printWriter = new PrintWriter(outputStream);
                    // 此处使用 println 来输出 response, 让结果中带有一个 \n 换行. 方便对端来接收解析.
    //                printWriter.println(response);
                    // flush 用来刷新缓冲区, 保证当前写入的数据, 一定会发送出去(println 输出后有可能会先放在缓冲区, 等缓冲区满会自动输出)
    //                printWriter.flush();
    
                    System.out.printf("[%s:%d] req: %s; resp: %s \n",
                            clientSocket.getInetAddress().toString(),
                            clientSocket.getPort(), request, response);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                // finally 里的内容保证一定可以执行到
                try {
                    clientSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public String process(String request) {
            return request;
        }
    
        public static void main(String[] args) throws IOException {
            TcpEchoServer server = new TcpEchoServer(9090);
            server.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    客户端

    1. 使用 Socket 创建客户端程序
    2. 调用 Socket 的构造方法时, 会自动进行 TCP 连接操作
    3. 发送请求数据 (数据这里是由键盘读入)
    4. 接收返回响应数据
    package network;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.net.Socket;
    import java.nio.charset.StandardCharsets;
    import java.util.Scanner;
    
    public class TcpEchoClient {
        private Socket socket = null;
    
        public TcpEchoClient(String ip, int port) throws IOException {
            // Socket 构造方法, 能够识别 点分十进制 格式的 ip 地址, 比 DatagramPacket 更方便.
            // new 对象的同时, 就会进行 Tcp 连接操作.
            socket = new Socket(ip, port);
        }
    
        public void start() {
            System.out.println("启动客户端");
            Scanner scanner = new Scanner(System.in);
            try(InputStream inputStream = socket.getInputStream();
                OutputStream outputStream = socket.getOutputStream()) {
                while(true) {
                    // 1. 先从键盘上读取用户输入的内容
                    System.out.print(">");
                    String request = scanner.next();
                    if(request.equals("bye")) {
                        System.out.println("goodbye");
                        break;
                    }
                    // 2. 把读取的内容构造成请求, 发送给服务器.
                    outputStream.write((request+"\n").getBytes(StandardCharsets.UTF_8));
                    outputStream.flush();
    //                PrintWriter printWriter = new PrintWriter(outputStream);
    //                printWriter.println(request);
    //                // flush 保证数据不会停留在缓冲区
    //                printWriter.flush();
    
                    // 3. 读取服务器的响应
                    Scanner respScanner = new Scanner(inputStream);
                    String response = respScanner.next();
                    // 4. 把响应内容显示到界面上
                    System.out.println(response);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) throws IOException {
            TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
            client.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    基于 UDP 的回显服务器设计

    服务端

    1. 使用 DatagramSocket 创建 Socket 对象, 来建立连接
    2. 读取请求数据 (使用 DatagramPacket 来接收数据 [输出型参数] )
    3. 根据请求计算响应
    4. 返回响应数据
    package network;
    
    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.net.SocketException;
    
    // UDP 版本的回显服务器
    public class UdpEchoServer {
        // 网络编程, 本质上是要操作网卡
        // 但是网卡不方便直接操作. 在 操作系统内核 中, 使用了一种特殊的叫做 "socket" 这样的文件来抽象表示网卡
        // 因此进行网络通信, 势必要现有一个 socket 对象
        private DatagramSocket socket = null;
    
        // 对于服务器来说, 创建 socket 对象的同时,要让他绑定上一个具体的端口号
        // 服务器一定要关联上一个具体的端口号!!!
        // 服务器是网络传输中, 被动的一方. 如果是操作系统随机分配的端口, 此时客户端就不知道这个端口是啥了, 也就无法进行通信了
        public UdpEchoServer(int port) throws SocketException {
            socket = new DatagramSocket(port);
        }
    
        public void start() throws IOException {
            System.out.println("服务器启动!");
            // 服务器不是只给一个客户端提供服务就完了. 需要服务很多客户端
            while(true) {
                // 只要有客户端过来, 就可以提供服务
                // 1. 读取客户端发来的请求是啥
                //    receive 方法的参数是一个输出型参数, 需要先构造好一个空白的 DatagramPacket 对象. 交给 receive 来进行填充
                DatagramPacket requestPacket = new DatagramPacket(
                        new byte[4096] ,0, 4096);
                socket.receive(requestPacket);
                // 此时这个 DatagramPacket 是一个特殊的对象, 但不方便直接进行处理. 可以把这里包含的数据拿出来, 构造成一个字符串, 以便处理
                // requestPacket.getLength(): 是数据内容的长度       requestPacket.getData().length: 是构成 packet 的字节数组的容量
                String request = new String(
                        requestPacket.getData(),
                        0, requestPacket.getLength()); //new String 后面的参数是要数据内容的长度
                // 2. 根据请求计算响应, 由于此处是回显服务器, 相应和请求相同
                String response = process(request);
                // 3. 把响应写回到客户端. send 的参数也是 DatagramPacket. 需要把这个 Packet 对象构造好.
                //    此处构造的响应对象, 不能是用空的字节数组构造了, 而是要使用响应数据来构造.                                        //SocketAddress 里面有 address 和 port (IP和端口号)
                DatagramPacket responsePacket = new DatagramPacket(
                        response.getBytes(), 0 ,
                        response.getBytes().length,requestPacket.getSocketAddress());
                socket.send(responsePacket);
                // 4. 打印本次请求相应的处理中间结果
                System.out.printf("[%s:%d] req: %s; resp: %s;\n",
                        requestPacket.getAddress().toString(),requestPacket.getPort(),
                        request, response);
            }
        }
    
        // 这个方法就是 "根据请求计算相应"
        public String process(String request) {
            return request;
        }
    
        public static void main(String[] args) throws IOException {
            // 端口号的指定, 1024 ~ 65535 均可
            UdpEchoServer server = new UdpEchoServer(9090);
            server.start();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    客户端

    1. 使用 DatagramSocket 来创建 Socket 对象
    2. 创建 DatagramPacket (内含 信息发送到的服务器信息 : ip, port)
    3. 发送请求数据 (DatagramPacket 中的服务器信息, 可以告诉 Socket 对象, “请把我发送给谁”)
    4. 接收响应数据
    package network;
    
    import java.io.IOException;
    import java.net.*;
    import java.util.Scanner;
    
    // UDP 版本的回显客户端
    public class UdpEchoClient {
        private DatagramSocket socket = null;
    
        // 存储的是服务器的 ip 和 端口号, 以便通讯使用
        private String serverIp;
        private int serverPort;
    
        // 一次通信, 需要有两个 ip, 两个 端口
        // 客户端的 ip : 127.0.0.1 已知
        // 客户端的 port 是系统自动分配
        // 服务器的 ip 和 端口, 需要手动告诉客户端, 才能顺利把消息发送给服务器
        public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
            socket = new DatagramSocket();
            this.serverIp = serverIp;
            this.serverPort = serverPort;
        }
    
        public void start() throws IOException {
            System.out.println("客户端启动!");
            Scanner sc = new Scanner(System.in);
    
            while(true) {
                // 1. 从控制台读取要发送的数据
                System.out.println(">");
                String request = sc.next();
                if(request.equals("bye")) {
                    System.out.println("goodbye!");
                    break;
                }
    
                // 2. 构造成 UDP 请求, 并发送
                //    构造这个 Packet 的时候, 需要把 severIp 和 port 都传进来. 但是此处 IP 地址需要填写一个 32 位的整数形式
                //    上述的 IP 地址是一个字符串. 需要使用 InetAddress.getByName 来进行一个转换.
                DatagramPacket requestPacket = new DatagramPacket(
                        request.getBytes(), 0, request.getBytes().length,
                        InetAddress.getByName(this.serverIp), this.serverPort);
                socket.send(requestPacket);
    
                // 3. 读取服务器的 UDP 响应, 并解析
                DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 0, 4096);
                socket.receive(responsePacket);
                String response = new String(responsePacket.getData(), 0 , responsePacket.getLength());
    
                // 4. 把解析好的结果显示出来
                System.out.printf("[%s:%d] response: %s; request: %s;\n",
                        responsePacket.getAddress().toString(),
                        responsePacket.getPort(), response, request);
    
            }
        }
    
        public static void main(String[] args) throws IOException {
            UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
            client.start();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

  • 相关阅读:
    华为机试题刷题总结
    Python | 机器学习之PCA降维
    HarmonyOS学习路之方舟开发框架—学习ArkTS语言(渲染控制 一)
    C++教程 - How to C++系列专栏第6篇
    C++内存检查
    蒙特卡洛树搜索方法介绍——Q规划与Dyna-Q算法
    //按层遍历二叉树,并收集结点
    AVR单片机开发8——EEPROM操作
    goroutine学习
    关于抽象类和抽象方法
  • 原文地址:https://blog.csdn.net/shianla/article/details/137976036