文章目录
- socket 套接字
- UDP 和 TCP 区别
- 1. 有连接 VS 无连接
- 2. 可靠传输 VS 不可靠传输
- 3. 面向字节流 VS 面向数据报
- 4. 全双工
- UDP 数据报套接字编程
- TCP 数据报套接字编程
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
DatagramSocket
是 UDP Socket
,用于发送和接收UDP数据报。
DatagramSocket
构造方法:
方法签名 | 方法说明 |
---|---|
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口 (一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用 于服务端) |
DatagramSocket
方法:
方法签名 | 方法说明 |
---|---|
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacketp) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
2.DatagramPacket
DatagramPacket
是 UDP 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 类 .
到此 本文结束 , 下文预告 网络原理 。