网络编程 -- socket 套接字

socket 套接字


本文就来学习一下 网络编程, 既然谈到了网络编程,那么要如何进行呢 ?


这里的核心就是 Socket API

在这里插入图片描述


Socket 就是操作系统,给应用程序,提供的网络编程 API


在上篇文章说过,在网络分层内传输层及其以下都是属于操作系统内核实现的,应用层是在应用程序内的,这里 socket api 就相当于 站在传输层的角度 与应用层进行交互的 ( 可以认为 socket api 是和 传输层密切相关的 )。


回忆一下 : 说过的传输层,是不是说过两个最核心的协议 UDP , TCP .


因此针对 传输层 中的 两种 核心的协议 , socket 推出了两种风格 API , 也就是 UDP 版本的 和 TCP 版本的 。(其实这里还有第三种风格 unix 域套接字, 只不过没人使用了,知道了也没啥用 ,这里就过个眼熟) .


这里我们想要学习 UDP 和 TCP 两种风格的API ,那么就需要对 UDP 和 TCP 这两种协议 有个简单的认识 , 所以下面就先来简单了解一下这两种协议

UDP 和 TCP 区别


这里先来看看 UDP 和 TCP 两种协议的 区别


UDP :

  • 无连接
  • 不可靠传输
  • 面向数据报
  • 全双工


TCP :

  • 有连接
  • 可靠传输
  • 面向字节流
  • 全双工

1. 有连接 VS 无连接


有连接 : 使用TCP 协议 ,需要连接成功才能够建立通信 ,连接建立需要对方来 接收 ,如果连接没建立好 ,通信不了. (简单来说就是 双方 需要数据传输的时候需要先建立联系,没有建立好就无法进行传输).


好比 打电话 :假设现在我要给女神打电话 , 拨号口,等待女神接听 ,等听到女神的 喂 , 就说明此时连接建立成功,可以进行交流了, 如果女神挂掉了,也会有对方正在繁忙的语音提示此时就说明没有连接建立成功,不能进行通话。


无连接 : 双方无需建立建立,直接发送消息 。


好比 : 发微信 发 qq 等 , 假设我 给女神发送早安 , 女神可能看到了也可能没看到,或者看到了但是不想回我们 。此时我们并不能知道了 女神 看没看到消息 ,这种情况就是 无连接 ,


总的来说 :TCP 就是要求双发先建立连接,连接好了,才能进行传数据。 而 UDP,直接传输数据,不需要双方建立连接。

2. 可靠传输 VS 不可靠传输


可靠传输 : 发送方知道 接收方 有没有接收到数据


注意 : 网络环境天然是复杂的 , 不可能保证传输的数据 100% 就能到达 , 比如说 拔网线 ,即便我们厉害 也不顶用。


所以可靠传输 , 传输的数据并不是百分百就能够成功的, 关键是看这里是否能 **感知 **到 传输的数据是否成功到达。


这里打电话就是可靠传输 : 比如说我们打电话,对方接听了, 我们说了一大堆事情,对方是否听到了我们是可以知道的,比如对方一直没有回应,我们就可以问一句,你听到了吗, 对方回应,此时我们就能够知道对方听到了 。


不可靠传输 : 发送方不知道接收方有没有接收到数据


这里的不可靠传输,同样于发微信或发qq消息 一样


还是发消息给女神,想要邀请女神来吃麻辣烫 ,假设将消息发送出去,

对方可以能不在线,此时无法看到消息,

对方可能太忙 (毕竟是女神吗,咋可能就一条舔狗呢) 。

对方看见了 但是不回你 (你都是舔狗 ,那不就是可有可无的吗 ,人家不会挺正常)

对方看见了 , 并接收了你的邀请消息没有发送成功 等


此时不管是那种情况,我们都无法知道 ,此时就相当于不可靠传输.


另外 : 有些软件,比如抖音 ,聊天有已读功能,此时发送消息,是能够知道对方是否接收到了 (看到了显示已读 ) 这种已读功能的就相当于可靠传输, QQ 微信 就没有这种已读功能, 就可以认为是不可靠传输 。


注意 : 可不可靠,和有没有连接没有任何关系


总的来说 : 你发送的消息 , 对方是否看的到 自己是知道的 心里有底对方看到了 ,那么就是 可靠的, 心里没底 , 不知道对方是否收到 那么就是不可靠的

3. 面向字节流 VS 面向数据报


面向字节流 : 数据传输就和 文件读写二 类似 “流式” 的


还记得 文件 IO 那片 吗, 这里的流 其实就是一种比喻的 说法,我们的数据可以 分多次发送

比如 : 发送 100 个字节的数据

可以采用 一次 传 100个字节, 一次 传 10 个字节 分 10次 ,一次传1个字节 分 100次 … 这里是不是就像水流一样。


面向数据报 : 数据传输 以 一个一个的 数据报 为基本单位 (一个数据报可能是诺干个字节,带有一定的格式 )

4. 全双工


全双工 : 一个通信通道,可以双向传输 (既可以发送,也可以接收)


图示 : 半双工 (了解即可)

在这里插入图片描述


简单了解完,UDP 和 TCP 的区别之后,下面 就来学习一下 ,两种风格的 Socket API 的使用 。


这里更详细的 UDP 和 TCP 协议 ,在后面的文章种会说到 ,不急 ,本主要还是对于 Socket 的使用.

UDP 数据报套接字编程


这里我们就基于 UDP 来编写一个简单的客户端服务器的网络通信程序


想要实现这个, 先来学习一下两个类 .


图示 :

在这里插入图片描述


1.DatagramSocket


DatagramSocketUDP Socket,用于发送和接收UDP数据报。


DatagramSocket 构造方法:

方法签名 方法说明
DatagramSocket() 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口
(一般用于客户端)
DatagramSocket(int port) 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用
于服务端)


DatagramSocket 方法:

方法签名 方法说明
void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacketp) 从此套接字发送数据报包(不会阻塞等待,直接发送)
void close() 关闭此数据报套接字


2.DatagramPacket


DatagramPacketUDP Socket 发送和接收的数据报。

DatagramPacket 构造方法:

方法签名 方法说明
DatagramPacket(byte[] buf, int length) 构造一个DatagramPacket以用来接收数据报,接收的数据保存在
字节数组(第一个参数buf)中,接收指定长度(第二个参数length)
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) 构造一个DatagramPacket以用来发送数据报,发送的数据为字节
数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号


DatagramPacket 方法:

方法签名 方法说明
InetAddressgetAddress() 从接收的数据报中,获取发送端主机IP地址;
或从发送的数据报中,获取接收端主机IP地址
int getPort() 从接收的数据报中,获取发送端主机的端口号;
或从发送的数据报中,获取接收端主机端口号
byte[] getData() 获取数据报中的数据


有了对这些方法的认识,就可以 写一个最简单的 UDP 版本的 客户端服务器程序 回显服务器 echo server 。


一个普通的服务器 : 收到请求, 更具请求计算响应 , 返回响应。


这里 就好比 去小馆子里 吃个炒粉, 我们向店长说来份炒粉,店长听到了 ,就将这个请求告诉了厨师 ,厨师接收到了请求,就去炒粉(更具请求计算出响应) ,然后将炒出来的粉 交给我们 (返回响应) .


这里我们的回显服务器 (echo server) 会省略其中的 “根据计算计算响应” 这个步骤, 相当于 请求是啥,就返回啥 (当前这个代码没有实际的业务, 这个代码也没啥太大作用 和意义 ,只是展示了 socket api 基础用法).作为一个真正的服务器, 一定是"根据请求计算响应 " 这个环节是最最重要的 , 这里的 根据请求计算响应 其实就相当于业务逻辑。


下面就来编写代码 :


图一 :

在这里插入图片描述


图二 :

在这里插入图片描述


图三 :

在这里插入图片描述


图四 :补充一点小细节
在这里插入图片描述


到此 UDP 服务器 就写完了


附上 : UDP 服务器代码 ,注意这里最后加了一个启动服务器的 main 方法


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], 4096);

            socket.receive(requestPacket);

            // 此时这个 DatagramPacket 是一个特殊的对象,并不方便直接进行处理,可以把这里包含的数据拿出来, 构造成一个字符串

            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());


            // 2. 根据请求计算响应 , 由于此时是一个回显服务器, 响应和请求相同

            String response = process(request);

            // 3.把响应写回到客户端  , send 的参数也是 DatagramPacket ,需要把这个 Packet 对象构造好

            // 此处构造的响应对象,不能是用空的字节数组构造了,而是要使用响应数据 (response字符)

            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getSocketAddress());

            // 将 计算出来的响应发送给客户端
            socket.send(responsePacket);

            // 4. 打印一下 ,当前这次请求响应的处理中间结果
            System.out.printf("[%s : %d] request : %s; response: %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.构造响应并写回给客户端 .


下面就来写 客户端代码


图一 :

在这里插入图片描述


图二 :

在这里插入图片描述


图三 :

在这里插入图片描述


附上客户端代码 :


import java.io.IOException;
import java.net.*;
import java.util.Scanner;

// UDP 版本的 回显客户端
public class UdpEchoClient {
    
    

    // 同样需要先创建一个 socket对象 , 来操作网卡
    private DatagramSocket socket = null;

    // 服务器 的 IP 地址
    private String serverIp;

    // 服务器 的 端口号
    private Integer serverPort;

    // 一次通信 ,需要由两个 ip , 两个端口
    // 客户端的 ip 是 127.0.0.1 已知.
    // 客户端的 port 是系统自动分配的
    // 服务器 ip 和 端口 也需要告诉客户端 , 才能顺利把消息发给服务器.
    public UdpEchoClient(String serverIp, Integer serverPort) throws SocketException {
    
    

        socket = new DatagramSocket();

        //这里需要 给 服务器的 ip 地址 和 端口号 ,相当于 买家了商品 ,需要提供 地址和电话 , 如果没有卖家咋发货呢?
        this.serverIp = serverIp;

        this.serverPort = serverPort;
    }

    // 启动 客户端
    public void start() throws IOException {
    
    

        System.out.println("客户端启动!");
        Scanner scanner = new Scanner(System.in);

        while (true) {
    
    
//        1. 从控制台读取发送的数据.
            System.out.println("-> ");

            String request = scanner.next();

            if (request.equals("exit")) {
    
    

                System.out.println("goodbye");
                break;
            }

//        2. 构造成 UDP 请求,并发送.
            // 构造这个 Packet 的时候 ,需要把 serverIp 和 port 都传入过来, 但是此处 IP 地址需要填写一个 32位的整数形式
            // 上述的 IP 地址是一个字符串 , 需要使用 InetAddress.getByName 来进行一个转换 .

            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIp), serverPort);
            // 构造好了请求发送
            socket.send(requestPacket);

//        3. 读取服务器的 UDP 响应,并解析.

            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);

            socket.receive(responsePacket);

            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());

//        4. 把解析好的结果显示出来.
            System.out.println(response);
        }


    }

    // 最后添加启动方法
    public static void main(String[] args) throws IOException {
    
    
        UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
        client.start();
    }

}


下面来执行一下 看看下效果 :

在这里插入图片描述


可以看到我们的客户端和服务器程序 ,是在我们自己的电脑上运行的, 而实际上,网络存在的意义,是跨主机通信.


那么我们的层序是否可以做到 跨主机通信的呢?


这里是可以的, 只不过需要外网 IP ,这里可以将服务器的代码 上传到 云服务器上,让后修改 客户端的 ServerIp 改为云服务器的, 就能够进行跨主机通信的。


这里就不演示了 ,比较麻烦 . 后面我们学习了 Linux , 购买了云服务器可以自己尝试一下.


下面继续 , 这里我们写的是回显服务器,缺少业务逻辑,这里就可以在上述代码的基础上稍作调正,实现一个 “查词典” 的服务器. (根据英文单词 , 翻译成中文解释)


附上代码 : 因为大部分代码是一样的 ,这里就可以通过继承来 ,重写 process 方法 , 来书写逻辑即可 .


// 对于 DictServer 来说 和 EchoServer 相比,大部分的东西都是一样的

// 主要是 "根据请求计算响应" 这个步骤不太一样 .

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

public class UdpDictServer extends UdpEchoServer {
    
    

    private Map<String, String> dict = new HashMap<>();

    public UdpDictServer(int port) throws SocketException {
    
    
        super(port);

        // 给这个 dict 设置内容
        dict.put("cat", "小猫");

        dict.put("dog", "小狗");

        dict.put("error", "错误");

        // 当然,这里可以无限多的设置键值对. .

    }


    @Override
    public String process(String request) {
    
    

        // 重写我们的 process 方法, 添加逻辑即可

        // 查词典的过程

        return dict.getOrDefault(request, "当前单词没有查询到结果!");

    }

    public static void main(String[] args) throws IOException {
    
    
        UdpDictServer server = new UdpDictServer(9090);
        server.start();
    }
}


这里关于上面这个程序还有一个小知识点 这里来补充一下 ,

针对上述的层序,来看看端口冲突 是啥效果 .

这里一个端口只能被一个进程使用 ,如果由多个使用,就不行。

这里具体来看一下咋样个不行法 。

在这里插入图片描述


这里 UDP socket api 使用就到这里 ,下满来 看看 TCP 的 socket api 的使用 .

TCP 数据报套接字编程


关于 TCP socket api 的这个 API , 难易程度 ,可以取决于你之前学习 IO 章节的掌握程度, 如果比较熟悉那么就容易, 如果不太熟悉或没学过,那么就可能有点困难.


这里就不多说了,下面就来愉快的学习一下 TCP 的 socket api 。


同样的这里 与 UDP 一样涉及到两个类


图示 :

在这里插入图片描述


ServerSocket API


ServerSocket 是创建TCP服务端Socket的API

ServerSocket 构造方法

方法签名 方法说明
ServerSocket(int port) 创建一个服务端流套接字Socket,并绑定到指定端口


ServerSocket 方法

方法签名 方法说明
Socket accept() 开始监听指定端口(创建时绑定的端口),有客户端连接后,
返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待
void close () 关闭此套接字


Socket API

Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端 Socket。

不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。


Socket 构造方法

方法签名 方法说明
Socket(String host , int port) 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接


Socket 方法

方法签名 方法说明
InetAddress getInetAddress() 返回套接字所连接的地址
InputStream getInputStream() 返回此套接字的输入流
OutputStream getOutputStream() 返回此套接字的输出流


看完这些 类和方法, 下面通过 写一个 TCP 版本的 回显服务器来 熟悉他们.


图一 :

在这里插入图片描述


图二 :

在这里插入图片描述


附上代码 :

package T_J4.T_1_23;

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.util.Scanner;

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("启动服务器");

        while (true) {
    
    

            // TCP 是有连接的, 因此不能一上来就读取数据, 需要先建立连接

            // 使用这个 clientSocket 和 具体的客户端进行交流 (建立连接).
            Socket clientSocket = serverSocket.accept();

            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 scanner = new Scanner(inputStream);
                if (!scanner.hasNext()) {
    
    
                    // 判断是否有下一个数据 , 没有下一个数据说明读完了 (客户端关闭连接) ;
                    System.out.printf("[%s : %d] 客户端下线\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                // 注意 !! 此处使用 next 是一直读取到换行符 / 空格 / 其他空白符结束 , 但是最终返回结果里不包含上述 空白符 .
                String request = scanner.next();

//                2. 根据请求构造响应
                String response = process(request);

//                3. 返回响应结果
//                outputStream 没有 write String 这样的功能 , 可以把 String 里的字节数组拿出来,进行 写入 .
                // 也可以用字符流来转换一下
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response); // 此处使用println 让结果中带有 \n 换行. 方便 对端来接受解析
                printWriter.flush(); // flush 用来刷新缓冲区 , 保证当前写入的数据, 确实是发送出去了

                System.out.printf("[%s : %d] request : %s , response %s \n", clientSocket.getInetAddress().toString(), clientSocket.getPort(),
                        request, response);

            }

        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            // finally 中的代码一定会被执行,所以 close 放在这里更加合适
            try {
    
    
                clientSocket.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }

    private String process(String request) {
    
    
        return request;
    }

    public static void main(String[] args) throws IOException {
    
    

        TcpEchoServer server = new TcpEchoServer(9090);

        server.start();
    }
}


这里 TCP版本的服务器写完了,下面来写 TCP 版本的客户端.


图一 :

在这里插入图片描述


图二 :

在这里插入图片描述


最后加上 main 方法 来启动 客户端 。


附上代码 :


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoClient {
    
    

    private Socket socket = null;

    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
    
    

        // Socket 构造方法 , 能够识别 点分十进制格式的 IP 地址 . 比 DatagramPacket 更方便 .
        // new 这个对象的同时 , 就会 进入 TCP 的连接操作.
        socket = new Socket(serverIp, serverPort);
    }


    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.printf("> ");

                String request = scanner.next();

                if (request.equals("exit")) {
    
    

                    System.out.println("goodbye");

                    break;
                }
                // 2. 把读到的内容构造成请求,发送给服务器

                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();
    }
}


这里执行以下, 来看一下效果如何 :

在这里插入图片描述


效果看完,这里提一个小问题 :

在这里插入图片描述


看完这个小问题 ,其实我们的代码还有一个大问题, 这里来演示一下 问题所在 :


图一 :

在这里插入图片描述


图二 :

在这里插入图片描述


优化 : 这里使用多线程,可以解决 一个服务器 只能处理一个客户端的问题, 但是客户端特别多,很多客户端频繁的来进行连接 , 此时就需要频繁的创建 / 销毁线程。


之前说过 创建 线程和 销毁线程 也是会消耗资源的 (虽然比 多进程 少) ,这里 频繁的创建和销毁 开销也是比较大的 。


那么 你能 回忆起之前学过的 知识 来优化它 吗 ?

想必你一定有了答案 , 没错就是线程池 , 下面再次调整我们的代码 :

在这里插入图片描述


补充一下 : TCP 的短链接 和 长连接

在这里插入图片描述


另外 : 这里虽然使用了线程池,但是还不够


如果 客户端非常多,而且客户端连接迟迟不肯断开,就会导致我们的机器上有很多线程 . (我们使用的线程池是自增的,觉得需要线程了就会自己创建) .


按照上面这种迟迟不肯断开的情况下 , 如果一个服务器 有几千个客户端,就的是几千个线程,如果是几万个客户端 , 就会有几万个线程 。


此时这种情况就会对我们的机器产生很大的负担 。


那么如何确解决这个问题呢 ?


方法一 : 采用多开服务器 ,但是多开服务器意味着成本的增加 , 得多加钱,明显是不好的选择 。


方法二 : 使用 IO 多路复用 (IO 多路转接)


关于这里的问题 ,其实就是 单机(单个服务器)如何支持更大量客户端的问题 , 这个问题 也可以称为 C 10M 问题.


C 10k 问题 : 单机处理 1w个客户端 , 这里 1w 个 ,我们通过 一些方法 ,还是能够解决的 。

C 10M 问题 : 但其处理 1Kw个 客户端 ,这里 不是说单机真的能处理 1kw 个 客户端,只是表达说 客户端比 C 10k 这里多很多 。


出现 C 10M 问题 的本质 就是 机器 承担不了 这么线程的开销 (每个线程处理一个客户端) ,


好比 你开了一家公司 , 你的公司 需要服务很多客户 ,然而你给每个客户都有一个专属的客服 , 因为你雇佣了很多客服 ,导致 月底了 你发不出工资 ,此时 你的公司自然就宣布 破产了。 。


这里解决办法很容易 ,不用雇佣那么多客服,而每名客服对接多名用户即可 ,这样就大大的减少了 客服人员 。


放在我们这里也是同理 ,我们想要一个线程处理多个客户端 连接,就可以采取 IO 多路复用 (IO 多路转接) , 别觉得这个东西很高大上,其实再生活中都接触过 。


比如说 : 买饭 , 假设 你 想吃 杂粮煎饼 , 你的弟弟想吃 牛肉面 , 你的 妹妹 想吃 肉夹馍 。


此时就有两种做法 :

1.采用单线程的方式 , 你作为老大 ,一个人去买 ,先去买你吃的 杂粮煎饼 ,然后去帮妹妹买 肉夹馍,最后卖牛肉面 .

2.采用多线程的方式 , 你说 要吃自己去买 ,然后就各买各的 。

3.还是采用单线程的方式 , 还是你去买饭 ,但是这次不同了,你来到 卖杂粮煎饼的摊子前 ,说老板给我来份杂粮煎饼 ,等会做好了我来那,然后你就跑到肉夹馍的摊子 如法炮制, 最后来到了 牛肉面的摊子 ,点了份牛肉面,此时那个好了就去拿那个 。


这三种方式 , 明显是第三种更好 ,我们的IO 复用就是这种操作 , 虽然是单线程 ,但是充分的利用了等待时间,再等待的过程中做其他事情。


这里我们IO 过程中 ,并不会一直持续的 ,IO过程也会有等待, 所以 IO 多路复用就抓住了这一点, 将这个等待时间充分利用起来, 做别的事情 .


知道了 IO 复用是啥 ,那么来处理 C 10M问题 。


思路 : 这里就可以 给线程安排个集合 ,这个集合就放了一堆连接 ,线程就负责监听这个集合,那个连接有数据来了 , 线程就来处理那个连接 … 这个其实就应用了一个事实 , 虽然连接很多 ,但是这些连接的请求并非严格意义上的同时,总还是有先有后的 。


解决 :多路复用 在操作系统里 ,提供了一些原生 API select , poll , epoll , 在 java 提供了一组 NIO 类 就封装了上述多路复用的 API , 我们就可以使用这个 类来解决问题


这里 思路 大于 API 的使用 , 这里就不继续下去了 ,如果感兴趣 可以自己去看看这个 NIO 类 .


到此 本文结束 , 下文预告 网络原理 。

猜你喜欢

转载自blog.csdn.net/mu_tong_/article/details/128792184