参考:
- 为什么网络IO会被阻塞
- 韩顺平网络多线程专题
- Java网络IO模型、阻塞与非阻塞、同步与异步
0、前言
0.1、IP、域名、端口号
0.1.1、IP
- IPv4使用4个字节(一个字节8位)表示,一个字节表示范围(0~255)。
- ip地址的组成=网络地址+主机地址,比如:192.168.16.92。
- IPv6使用16个字节表示,是IPv4地址长度的四倍。
- Ipv4地址分类:
0.1.2、域名
- 例如:
www.baidu.com 。 - 好处:为了方便记忆,解决记ip的困难。
- 概念:将ip地址映射成域名。
0.1.3、端口号
- 用于标识计算机上某个特定的网络程序。
- 范围:0~65535(常见的:mysql3306,tomcat8080,http80,…)。
0.1.4、数据进入协议栈的封装过程
0.2、Socket是什么?
- Socket(套接字)开发网络应用程序被广泛采用,以至于称为事实标准。
- 通信的两端都要有Socket,是两台机器之间通信的断点。
- 网络通信本质就是Socket通信。
- SOcket允许程序把网络连接当成一个流,数据在两个Socket之间通过IO传输。
- 一般主动发起通信的应用程序属于客户端,等待通信请求的为服务端。
- 通过Socket可以实现通信的读写数据。
- 一定要关闭socket,否则一旦连接过多,之后新的客户端再想连接就无法连接成功。
- 服务端指定端口号进行监听,客户端也会被随机分配一个端口进行通信,该端口是TCP/IP来分配的,是不确定的,随机的。
1、I/O是什么?
- I/O就是input和output的缩写,即:输入/输出。
- 磁盘I/O指的是硬盘和内存之间的输入和输出。
- 读取本地文件的时候,要将磁盘的数据拷贝到内存中,修改本地文件的时候,需要把修改后的数据拷贝到磁盘中。
- 当网络上的数据到来时,网卡需要将数据拷贝到内存中。当要发送数据给网络上的其他人时,需要将数据从内存拷贝到网卡里。
- 那为什么都要跟内存交互呢?
- 我们的指令最终是由 CPU 执行的,究其原因是 CPU 与内存交互的速度远高于 CPU 和这些外部设备直接交互的速度。
- 因此都是和内存交互,当然假设没有内存,让 CPU 直接和外部设备交互,那也算 I/O。
- 总结下:I/O 就是指内存与外部设备之间的交互(数据拷贝)。
1.1 阻塞式I/O
package com.morris.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class BioSingleThreadServer {
public static final int PORT = 8899;
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(PORT));
System.out.println("server is start at " + PORT);
while (true) {
try {
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
System.out.println("connect success " + socket.getPort());
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
System.out.println("receive from client: " + reader.readLine());
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
1.1.1、创建socket(服务端)
- 创建socket:首先服务端需要先创建一个socket。在Linux中一切都是文件,那么socket也是文件,每个文件都有一个整型的文件描述符(fd)来指代这个文件。
int socket(int domain, int type, int protocol);
- domain:这个参数用于选择通信的协议族,比如选择 IPv4 通信,还是 IPv6 通信等等
- type:选择套接字类型,可选字节流套接字、数据报套接字等等。
- protocol:指定使用的协议。通常可以设置为0,因为可以由前面两个参数推出说需要使用的协议。
- 比如
socket(AF_INET, SOCK_STREAM, 0); ,表明使用 IPv4 ,且使用字节流套接字,可以判断使用的协议为 TCP 协议。 - 返回值为int,就是创建的socket的id。
1.1.2、bind(服务端)
- 此时创建了一个socket,但还没有地址指向这个socket。
- 服务器应用需要指明IP和端口,这样客户端才能指定与服务器那个端口进行通讯。此时需要指定一个地址和端口来与这个socket绑定。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); ,参数里面的sockfd就是创建的socket的文件描述符,执行了bind参数之后,socket被访问更近一步了。
1.1.3、listen(服务端)
- 此时socket还处于closed状态,即:不对外监听的,需要调用listen方法让socket进入被动监听的状态。
int listen(int sockfd, int backlog); ,sockfd是socket的的文件描述符,此时还要指明backlog。backlog就是队列的大小(socket有一个队列,同时存放已经完成的连接和半连接,backlog为这个队列的大小)。
1.1.4、accept(服务端)
- 初始化好监听套接字,此时会有客户端连接上来,需要处理这些已经完成建连的连接。
- 三次握手的连接会被加入到已完成连接队列中,此时需要从已经完成连接队列中拿到连接进行处理,拿取的动作由accept完成。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); - 如果已经完成连接队列没有连接可以取,那么调用accept的线程就会阻塞等待。
1.1.5、connect(客户端)
- 客户端也需要创建一个socket,也就是调用
socket() 。 - 客户端创建完socket并调用
connect 之后,连接就处于SYN_SEND 状态,当收到服务端回传的连接确认报文段之后,连接就变为ESTABLISHED 状态,此时三次握手完毕。 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); ,调用connect 需要指定远程的地址和端口进行建立连接,三次握手完毕之后就可以开始通信了。- 客户端这边是不需要调用bind操作,默认会选择源IP和随机端口
此时从连接角度看,就有两个阻塞的点:
- connect:需要阻塞等待三次握手完成。
- accept:需要等待可用的已完成的连接,如果已完成连接队列为空,则被阻塞。
1.1.6、read、write
连接建立好之后就要进行读写数据。
- read 为读数据,从服务端来看就是等待客户端的请求,如果客户端不发请求,那么调用 read 会处于阻塞等待状态,没有数据可以读,这个应该很好理解。
- write 为写数据,一般而言服务端接受客户端的请求之后,会进行一些逻辑处理,然后再把结果返回给客户端,这个写入也可能会被阻塞。
- 由于采用TCP协议,TCP协议需要保证数据可靠、有序传输,并且给予段与段之间的流量控制,所以说发送不是直接发出去,它有个发送缓冲区,我们需要把数据先拷贝到 TCP 的发送缓冲区,由 TCP 自行控制发送的时间和逻辑,有可能还有重传什么的。
- 如果我们发的过快,导致接收方处理不过来,那么接收方就会通过 TCP 协议告知:别发了!忙不过来了。发送缓存区是有大小限制的,由于无法发送,还不断调用 write 那么缓存区就满了,满了就不然你 write 了,所以 write 也会发生阻塞。
1.1.6、总结
- 因为建连和通信涉及到的 accept、connect、read、write 这几个方法都可能会发生阻塞。
- 阻塞会占用当前执行的线程,使之不能进行其他操作,并且频繁阻塞唤醒切换上下文也会导致性能的下降。
|