一、TCP通信
1.1 TCP与UDP
1.2 TCP通信流程
通信的流程
服务器端 (被动接受连接的角色)
1. 创建一个用于监听的套接字
- 监听:监听有客户端的连接
- 套接字:这个套接字其实就是一个文件描述符
2. 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
- 客户端连接服务器的时候使用的就是这个IP和端口
3. 设置监听,监听的fd开始工作
4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(fd)
5. 通信
- 接收数据
- 发送数据
6. 通信结束,断开连接
客户端(主动连接的角色)
1. 创建一个用于通信的套接字(fd)
2. 连接服务器,需要指定连接的服务器的 IP 和 端口
3. 连接成功了,客户端可以直接和服务器通信
- 接收数据
- 发送数据
4. 通信结束,断开连接
1.3 套接字函数
#include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略 int socket(int domain, int type, int protocol); - 功能:创建一个套接字 - 参数: - domain: 协议族 - AF_INET : ipv4 - AF_INET6 : ipv6 - AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信) - type: 通信过程中使用的协议类型 - SOCK_STREAM : 流式协议 - SOCK_DGRAM : 报式协议 - protocol : 具体的一个协议。一般写0 - SOCK_STREAM : 流式协议默认使用 TCP - SOCK_DGRAM : 报式协议默认使用 UDP - 返回值: - 成功:返回文件描述符,操作的就是内核缓冲区。 - 失败:-1 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命名 - 功能:绑定,将fd 和本地的IP + 端口进行绑定 - 参数: - sockfd : 通过socket函数得到的文件描述符 - addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息 - addrlen : 第二个参数结构体占的内存大小 int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn - 功能:监听这个socket上的连接 - 参数: - sockfd : 通过socket()函数得到的文件描述符 - backlog : 未连接的和已经连接的和的最大值, 5 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); - 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接 - 参数: - sockfd : 用于监听的文件描述符 - addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port) - addrlen : 指定第二个参数的对应的内存大小 - 返回值: - 成功 :用于通信的文件描述符 - -1 : 失败 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); - 功能: 客户端连接服务器 - 参数: - sockfd : 用于通信的文件描述符 - addr : 客户端要连接的服务器的地址信息 - addrlen : 第二个参数的内存大小 - 返回值:成功 0, 失败 -1 ssize_t write(int fd, const void *buf, size_t count); // 写数据 ssize_t read(int fd, void *buf, size_t count); // 读数据
二、TCP通信,服务器端和客户端实现
实例为回射服务器(客户端发什么,服务器端回什么)
服务器端:
/* 实现TCP通信的服务器端 */ #include<stdio.h> #include <arpa/inet.h> #include<unistd.h> #include<stdlib.h> #include<string.h> int main(){ //1.创建用于监听套接字 int lfd = socket(AF_INET,SOCK_STREAM,0); if(lfd==-1){ perror("socket:"); exit(0); } //2.绑定端口号和IP地址 //初始化sockaddr_in结构体,需要注意IP地址到网络的转换,以及端口号的字节序转换 struct sockaddr_in sock_addr; sock_addr.sin_family = AF_INET; //inet_pton(AF_INET, "192.168.37.129", sock_addr.sin_addr.s_addr); //服务器端开发可以直接指定s_addr=0 sock_addr.sin_addr.s_addr = 0; sock_addr.sin_port = htons(9999); //绑定 int ret = bind(lfd, (struct sockaddr *)&sock_addr,sizeof(sock_addr)); if(ret==-1){ perror("bind:"); exit(0); } //3.监听有无客户端连接 ret = listen(lfd,8); if(ret==-1){ perror("listen:"); exit(0); } //4.接收客户端连接 struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr); int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);//返回文件描述符 if(cfd==-1){ perror("accept:"); exit(0); } //输出客户端的信息,网络字节序转主机字节序 char clientIP[16]; inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP)); unsigned short clientPort = ntohs(clientaddr.sin_port); printf("Client IP is: %s, port is : %d\n", clientIP, clientPort); char rebuf[1024]={0}; while(1){ //5.获取客户端的数据,给客户端发送数据 int lens = read(cfd, rebuf, sizeof(rebuf)); if(lens==-1){ perror("read:"); exit(0); }else if(lens>0){ printf("recv form Client: %s\n", rebuf); }else if(lens==0){ //表示客户端断开连接 printf("Client closed.........\n"); break; } //发送数据 char *wrdata; strcpy(wrdata, rebuf); write(cfd, wrdata, strlen(wrdata)); } //关闭文件描述符 close(cfd); close(lfd); return 0; }
客户端:
/* 实现TCP通信的客户端 */ #include<stdio.h> #include <arpa/inet.h> #include<unistd.h> #include<stdlib.h> #include<string.h> int main(){ //1.创建套接字 int fd = socket(AF_INET,SOCK_STREAM,0); if(fd==-1){ perror("socket:"); exit(0); } //2.连接服务器端 struct sockaddr_in sock_addr; sock_addr.sin_family = AF_INET; inet_pton(AF_INET, "192.168.37.129", &sock_addr.sin_addr.s_addr); //服务器端开发可以直接指定s_addr=0 //sock_addr.sin_addr.s_addr = 0; sock_addr.sin_port = htons(9999); int ret = connect(fd, (struct sockaddr *)&sock_addr, sizeof(sock_addr)); if(ret==-1){ perror("connect:"); exit(0); } char rebuf[1024]={0}; while(1){ //3.给服务器端发送数据 char * wrbuf="hello, hi,hello"; write(fd, wrbuf, strlen(wrbuf)); //4.读取服务器端的数据 sleep(1); int len = read(fd, rebuf, sizeof(rebuf)); if(len==-1){ perror("read:"); exit(0); }else if(len>0){ printf("recv form Sever: %s\n", rebuf); }else if(len==0){ //表示服务器端断开连接 printf("Sever closed.........\n"); break; } } //关闭连接 close(fd); return 0; }
完成键盘输入通信,只需改为标准输入即可。
三、TCP通信3次握手4次挥手
首先是TCP报文段六个标志位的含义:
URG 标志,表示紧急指针(urgent pointer)是否有效。
ACK 标志,表示确认号是否有效。我们称携带 ACK 标志的 TCP 报文段为确认报文段。
PSH 标志,提示接收端应用程序应该立即从 TCP 接收缓冲区中读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在 TCP 接收缓冲区中)。
RST 标志,表示要求对方重新建立连接。我们称携带 RST 标志的 TCP 报文段为复位报文段。
SYN 标志,表示请求建立一个连接。我们称携带 SYN 标志的 TCP 报文段为同步报文段。
FIN 标志,表示通知对方本端要关闭连接了。我们称携带 FIN 标志的 TCP 报文段为结束报文段。
其次是序号以及确认序号的意义:
32 位序号(sequence number):一次 TCP 通信(从 TCP 连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。假设主机 A 和主机 B 进行 TCP 通信,A 发送给 B 的第一个TCP 报文段中,序号值被系统初始化为某个随机值 ISN(Initial Sequence Number,初始序号值)。那么在该传输方向上(从 A 到 B),后续的 TCP 报文段中序号值将被系统设置成 ISN 加上该报文段所携带数据的第一个字节在整个字节流中的偏移。例如,某个 TCP 报文段传送的数据是字节流中的第 1025 ~ 2048 字节,那么该报文段的序号值就是 ISN + 1025。另外一个传输方向(从B 到 A)的 TCP 报文段的序号值也具有相同的含义。
32 位确认号(acknowledgement number):用作对另一方发送来的 TCP 报文段的响应。其值是收到的 TCP 报文段的序号值 + 标志位长度(SYN,FIN) + 数据长度 。假设主机 A 和主机 B 进行TCP 通信,那么 A 发送出的 TCP 报文段不仅携带自己的序号,而且包含对 B 发送来的 TCP 报文段的确认号。反之,B 发送出的 TCP 报文段也同样携带自己的序号和对 A 发送来的报文段的确认序号。
3.1 TCP三次握手
第一次握手:
1.客户端将SYN标志位置为1
2.生成一个随机的32位的序号seq=J ,这个序号后边是可以携带数据(数据的大小)。
第二次握手:
1.服务器端接牧客户端的连接:ACK=1
2.服务器会回发一个确认序号:ack=客户端的序号+数据长度+SYN/FIN(按一个字节算)
3.服务器端会向客户端发起连接请求:SYN=1
4.服务器会生成一个随机序号:seq =K
第三次握手:
1.客户单应答服务器的连接请求:ACK=1
2.客户端回复收到了服务器端的威据:ack=服务端的序号+数据长度+SYN/FIN(按一个字节算
握手比喻:
3.2 滑动窗口拥塞控制
滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(称窗口尺寸)。
TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0时,发送方一般不能再发送数据报。
滑动窗口是 TCP 中实现诸如 ACK 确认、流量控制、拥塞控制的承载结构。
窗口理解为缓冲区的大小
滑动窗口的大小会随着发送数据和接收数据而变化。
通信的双方都有发送缓冲区和接收数据的缓冲区
服务器:
发送缓冲区(发送缓冲区的窗口)
接收缓冲区(接收缓冲区的窗口)
客户端
发送缓冲区(发送缓冲区的窗口)
接收缓冲区(接收缓冲区的窗口)
发送方的缓冲区:白色格子:空闲的空间。灰色格子:数据已经被发送出去了,但是还没有被接收紫色格子:还没有发送出去的躞效据
接收方的缓冲区:白色格子:空闲的空间。紫色格子:已经接收到的数据
滑动窗口举例
# mss: Maximum Segment Size(一条数据的最大的数据量)
# win: 滑动窗口
1. 客户端向服务器发起连接,客户单的滑动窗口是4096,一次发送的最大数据量是1460
2. 服务器接收连接情况,告诉客户端服务器的窗口大小是6144,一次发送的最大数据量是1024
3. 第三次握手
4. 4-9 客户端连续给服务器发送了6k的数据,每次发送1k
5. 第10次,服务器告诉客户端:发送的6k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了2k,窗口大小是2k
6. 第11次,服务器告诉客户端:发送的6k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了4k,窗口大小是4k
7. 第12次,客户端给服务器发送了1k的数据
8. 第13次,客户端主动请求和服务器断开连接,并且给服务器发送了1k的数据
9. 第14次,服务器回复ACK 8194, a:同意断开连接的请求 b:告诉客户端已经接受到方才发的2k的数据c:滑动窗口2k
10.第15、16次,通知客户端滑动窗口的大小
11.第17次,第三次挥手,服务器端给客户端发送FIN,请求断开连接
12.第18次,第四次挥手,客户端同意了服务器端的断开请求
3.3 TCP四次挥手
四次挥手发生在断开连接的时候,在程序中当调用了close()会使用TCP协议进行四次挥手。客户端和服务器端都可以主动发起断开连接,谁先调用close()谁就是发起。因为在TCP连接的时候,采用三次握手建立的的连接是双向的,在断开的时候需要双向断开。
挥手比喻: