1. 简单的示例伪程序
服务端伪代码:
<span style="font-family:Microsoft YaHei;font-size:14px;">// 1. 建立一个监听socket listen_socket = socket(AF_INET, SOCK_STREAM, 0); // 2. 创建一个监听socket绑定的本地地址结构体 serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 使用通配地址 serverAddr.sin_port = htons(5500); // 5500是服务器的监听端口号 int ret = bind(listen_socket, (sockaddr*)&serverAddr, sizeof(serverAddr) ); // 3. 开始监听客户端请求 listen(listenfd, SOMAXCONN); // 注意这里的 SOMAXCONN 参数 // 4. 循环接受客户端的请求 while (true){ // 5. accept以阻塞方式工作,当有一个连接进来时,为这个连接产生一个业务socket:business_socket business_socket = accept(listenfd, (sockaddr*)NULL, NULL); // 6. 向业务socket:business_socket中写入数据 send(business_socket, buf, strlen(buf), 0 ); // 7. 关闭业务socket close(business_socket); } </span>
<span style="font-family:Microsoft YaHei;font-size:14px;">//1.创建一个客户端使用的socket client_socket = socket(AF_INET, SOCK_STREAM, 0) //2.创建一个地址结构体,用于存储服务端的地址信息 servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5500); servaddr.sin_addr.s_addr = inet_addr(server_ip); //3.连接服务器程序 connect(client_socket, (struct sockaddr*) &servaddr, sizeof(servaddr)) //4.从socket中读取数据到recvline while ( (n = read(client_socket, recvline, MAXLINE)) > 0) { //5.处理存储在recvline中的内容 } </span>
通过上图可以看到TCP的工作过程如下:
[1] 服务器端先启动,创建一个监听socket,将这个socket绑定到一个公开端口,然后监听这个socket以等待客户端的连接请求发送过来;
[2] 客户端后启动,创建一个socket,然后用它连接到服务端的监听socket;
[3] 服务端在监听socket等着客户端连接进来之后,在服务端为这个连接产生一个业务socket,后续服务端就用此业务socket给这个客户端通信;
[4] 客户端在连接建立后,就可以向服务端发送请求数据;
[5] 客户端在请求满足之后,主动关闭连接;(1) 连接建立时的socket五元组变化情况
假设上述的服务端程序运行在192.168.1.201上,客户端运行在192.168.1.202上,根据前面的表述,一个socket可以认为是由一个五元组标识:<本地地址,本地端口,远端地址,远端端口,通信协议>,用*表示一个未确定项(通配项),下面的过程将以这个五元组的变化来描述客户端和服务器的工作过程(图片中黄色代码本地的地址和端口,红色为远端地址和端口):
[1] 服务端创建一个socket,执行代码: listen_socket = socket(AF_INET, SOCK_STREAM,0)后这个socket的五元组的样子是:<*,*,*,*,TCP>,即这时只知道这个socket用TCP协议通信,但是还不知道TCP对应的源端口、IP和目的端口、IP;
[2] 服务端创建一个监听地址,并将监听socket绑定到这个监听地址上,经过该步骤之后,监听socket对应五元组的本地端口号已经被设置为5500:
[3] 服务端执行监听操作之后将在监听socket上监听客户端的连接请求;
[4] 客户端创建一个socket,与服务端创建socket时一样,这里创建时只指定了socket五元组的通信协议:TCP;
[5] 客户端执行connect操作,该操作中将指定要连接的服务端的IP地址和端口号,本地的IP地址和端口号由OS帮我们填写,然后与服务器端建立连接,即执行三次握手操作;
[6] 服务端的监听socket接到客户端连接请求之后将为之产生一个业务socket,业务socket的本地地址和端口都有监听socket一致,监听socket指定本地地址,业务socket里要指定,业务socket的远端地址和端口来自这个新连接中,上层业务通过accept获得这个新业务socket;
[8] 连接建立之后,客户端与服务端之间使用业务socket进行通信
2.3 数据交付过程
问题描述:每台主机可能很多socket在使用,这些socket可能分属于不同的应用进程,但是在操作系统内,他们都是一样的,那么数据到来之后,操作系统是怎么判断数据是属于哪个socket的呢?
在UNIX操作系统内部,每个socket中都包含一个协议控制块,协议控制块中将有每个socket对应的五元组信息,这些协议控制块将被串联起来放在一个链表中,socket结构图和协议控制块中分别有指针指向彼此;如下图TCP的协议控制块链表:
协议控制块属于传输层的组成部分,如下图绿色框所示,TCP和UDP分别有自己独立的链表。
当TCP收到来自IP层的数据报后,将查找协议控制块列表,通过对比数据报文中端口号等信息,找到通配匹配数最小的协议控制块,即是目标协议控制块中,由于协议控制块中有指针指向对应的socket,也就找到了数据应该交付到哪个socket中,然后将数据放在socket的接收buffer中即可;这里通配匹配数是TCP的本地地址、本地端口、远端地址、远端端口这四项中,在没有确定不匹配项的前提下,用通配符*所匹配的项数,通配匹配数越小,说明匹配度越高;
例如,假设当前server端主机只有一个监听socket和三个业务socket,如下: 本地地址 |
本地端口 |
远端地址 |
远端端口 |
TCP状态 |
* |
5500 |
* |
* |
LISTEN |
192.168.1.201 |
5500 |
192.168.1.202 |
3500 |
ESTABLISH |
192.168.1.201 |
5500 |
192.168.1.202 |
3501 |
ESTABLISH |
192.168.1.201 |
5500 |
192.168.1.203 |
3501 |
ESTABLISH |
|
|
|
|
|
本地地址 |
本地端口 |
远端地址 |
远端端口 |
通配匹配数 |
* |
5500 |
* |
* |
3 |
192.168.1.201 |
5500 |
192.168.1.202 |
3500 |
0 |
192.168.1.201 |
5500 |
192.168.1.202 |
3501 |
端口不匹配 |
192.168.1.201 |
5500 |
192.168.1.203 |
3501 |
Ip和端口不匹配 |
|
|
|
|
|
第一个socket匹配当前报文段时,只有本地端口5500是完全匹配,本地地址、远端地址和远端端口都是使用通配符*匹配的,因此通配匹配数为3;
第二个socket匹配当前报文段时,全部精确匹配,使用通配符*匹配的项数为0;
第三个socket匹配当前报文段时,其远端端口3501与报文的源端口3500不匹配,因此排除此项;
第四个socket匹配当前报文段时,其远端端口3501和远端IP192.168.1.203与报文段的源端口3500和源IP192.168.1.202都不匹配,因此排除此项;
通过上述的比较,可以得到最小匹配数为0的第二个socket的协议控制块的结构体(PCB),通过此PCB就能知道它对应的socket结构体,接下来就可以将数据放到此socket结构体的接收缓存中。