赞
踩
TCP 是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务器的内存里保存的一份关于对方的信息,如 IP 地址、端口号等。
TCP 可以看成是一种字节流,它会处理 IP 层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在TCP头部。
TCP 提供了一种可靠、面向连接、字节流、传输层的服务,
用采 三次握手 建立一个连接;
采用 四次挥手 关闭一个连接。
三次握手发生在客户端连接的时候,当调用connect(),底层会通过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(按一个字节算)
四次挥手发生在断开连接的时候,在程序中当调用了close()
会使用TCP协议进行四次挥手。
客户端和服务器端都可以主动发起断开连接,谁先调用close()
,谁就发起。
在TCP连接的时候,采用三次挥手建立的连接时双向的,在断开的时候需要双向断开。
滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(称窗口尺寸)。
TCP中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0时,发送方一般不能再发送数据报。
滑动窗口是 TCP 中实现诸如 ACK 确认、流量控制、拥塞控制的承载结构。
窗口理解为缓冲区的大小,滑动窗口的大小会随着发送数据和接收数据而变化。通信的双方都有发送数据和接受数据的缓冲区。
服务器:
发送缓冲区(发送滑动窗口)
接收缓冲区(接收滑动窗口)
客户端:
发送缓冲区(发送滑动窗口)
接收缓冲区(接收滑动窗口)
发送方的缓冲区:
白色格子:空闲的空间
灰色格子:数据已经被发送出去,但还没有被接收
紫色格子:还没有发送出去的数据
接收方的缓冲区:
白色格子:空闲的空间
紫色格子:已经接收到的数据
mss : 一条数据的最大的数据量
win : 滑动窗口
1. 第一次握手,客户端向服务器发起连接,客户端的滑动窗口是4096,一次发送的最大数据量是4096
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:同意断开连接的请求,告诉客户端已经接收到刚才发送的2k数据,滑动窗口大小为2k
10.第15-16次,通知客户端滑动窗口大小
11.第17次,第三次挥手,服务器端给客户端发送给FIN,请求断开连接
12.第18次,第四次挥手,客户端统一服务器端的断开请求
要实现TCP通信服务器并发的任务,使用多线程或者多进程来解决。
思路:
1.一个父进程,多个子进程
2.父进程负责等待并接收客户端连接
3.子进程:完成通信,接收一个客户端连接,就创建一个子进程用于通信
服务器端:
#include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <wait.h> #include <errno.h> void recyleChild(int arg) { while(1) { int ret = waitpid(-1, NULL, WNOHANG); if(ret == -1) { // 所有的子进程都回收了 break; }else if(ret == 0) { // 还有子进程活着 break; } else if(ret > 0){ // 被回收了 printf("子进程 %d 被回收了\n", ret); } } } int main() { struct sigaction act; act.sa_flags = 0; sigemptyset(&act.sa_mask); act.sa_handler = recyleChild; // 注册信号捕捉 sigaction(SIGCHLD, &act, NULL); // 创建socket int lfd = socket(PF_INET, SOCK_STREAM, 0); // 创建socket地址并对成员初始化 struct sockaddr_in saddr; saddr.sin_family = AF_INET; saddr.sin_port = htons(9999); saddr.sin_addr.s_addr = INADDR_ANY; // 绑定 int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr)); // 监听 ret = listen(lfd, 128); // 不断循环等待客户端连接 while(1) { struct sockaddr_in cliaddr; // 连接的客户端信息 int len = sizeof(cliaddr); // 接受连接 int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len); if(cfd == -1) { if(errno == EINTR) { continue; } perror("accept"); exit(-1); } // 每连接一个客户端,就创建一个子进程跟客户端通信 pid_t pid = fork(); if(pid == 0) { // 子进程 // 获取客户端的信息 char cliIp[16]; inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp)); unsigned short cliPort = ntohs(cliaddr.sin_port); printf("client ip is : %s, prot is %d\n", cliIp, cliPort); // 接收客户端发来的数据 char recvBuf[1024]; while(1) { int len = read(cfd, &recvBuf, sizeof(recvBuf)); if(len == -1) { perror("read"); exit(-1); }else if(len > 0) { printf("recv client : %s\n", recvBuf); } else if(len == 0) { printf("client closed....\n"); break; } write(cfd, recvBuf, strlen(recvBuf) + 1); } close(cfd); exit(0); // 退出当前子进程 } } close(lfd); return 0; }
客户端:
// TCP通信的客户端 #include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include <stdlib.h> int main() { // 1.创建套接字 int fd = socket(AF_INET, SOCK_STREAM, 0); // 2.连接服务器端 struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; inet_pton(AF_INET, "192.168.193.128", &serveraddr.sin_addr.s_addr); serveraddr.sin_port = htons(9999); int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); // 3. 通信 char recvBuf[1024]; int i = 0; while(1) { sprintf(recvBuf, "data : %d\n", i++); // 给服务器端发送数据 write(fd, recvBuf, strlen(recvBuf)+1); int len = read(fd, recvBuf, sizeof(recvBuf)); if(len == -1) { perror("read"); exit(-1); } else if(len > 0) { printf("recv server : %s\n", recvBuf); } else if(len == 0) { // 表示服务器端断开连接 printf("server closed..."); break; } sleep(1); } // 关闭连接 close(fd); return 0; }
#include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <pthread.h> struct sockInfo { int fd; // 通信的文件描述符 struct sockaddr_in addr; pthread_t tid; // 线程号 }; struct sockInfo sockinfos[128]; void * working(void * arg) { // 子线程和客户端通信 cfd 客户端的信息 线程号 // 获取客户端的信息 struct sockInfo * pinfo = (struct sockInfo *)arg; char cliIp[16]; inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp)); unsigned short cliPort = ntohs(pinfo->addr.sin_port); printf("client ip is : %s, prot is %d\n", cliIp, cliPort); // 接收客户端发来的数据 char recvBuf[1024]; while(1) { int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf)); if(len == -1) { perror("read"); exit(-1); }else if(len > 0) { printf("recv client : %s\n", recvBuf); } else if(len == 0) { printf("client closed....\n"); break; } write(pinfo->fd, recvBuf, strlen(recvBuf) + 1); } close(pinfo->fd); return NULL; } int main() { // 创建socket int lfd = socket(PF_INET, SOCK_STREAM, 0); if(lfd == -1){ perror("socket"); exit(-1); } struct sockaddr_in saddr; saddr.sin_family = AF_INET; saddr.sin_port = htons(9999); saddr.sin_addr.s_addr = INADDR_ANY; // 绑定 int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr)); if(ret == -1) { perror("bind"); exit(-1); } // 监听 ret = listen(lfd, 128); if(ret == -1) { perror("listen"); exit(-1); } // 初始化数据 int max = sizeof(sockinfos) / sizeof(sockinfos[0]); for(int i = 0; i < max; i++) { bzero(&sockinfos[i], sizeof(sockinfos[i])); sockinfos[i].fd = -1; sockinfos[i].tid = -1; } // 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信 while(1) { struct sockaddr_in cliaddr; int len = sizeof(cliaddr); // 接受连接 int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len); struct sockInfo * pinfo; for(int i = 0; i < max; i++) { // 从这个数组中找到一个可以用的sockInfo元素 if(sockinfos[i].fd == -1) { pinfo = &sockinfos[i]; break; } if(i == max - 1) { sleep(1); i--; } } pinfo->fd = cfd; memcpy(&pinfo->addr, &cliaddr, len); // 创建子线程 pthread_create(&pinfo->tid, NULL, working, pinfo); pthread_detach(pinfo->tid); } close(lfd); return 0; }
状态转换发生在三次握手和四次挥手过程中,数据传输过程中状态不会改变。
三次握手:
第一次握手,客户端调用connect将SYN置为1发送报文头,此时客户端的状态从close()转换为SYN_SENT;
第二次握手,服务器端一直调用listen(),状态为listen()状态,当接收到客户端发送的SYN,此时服务器端的状态从listen()状态转换为SYN_RCVD,并给回给客户端SYN、ACK;
第三次握手,当客户端接收到服务器端发送的SYN、ACK,此时客户端的状态转换为ESTABLISHED,并给回给服务器端ACK,此时服务器端的状态转换为ESTABLISHED。
四次挥手:
假设由客户端发起第一次挥手:
第一次挥手,客户端向服务器端发送FIN请求,此时客户端的状态转换为FIN_WAIT_1;
第二次挥手,服务器端接收到FIN请求,此时服务器端的状态转换为CLOSE_WAIT,并回给客户端ACK,客户端接收到ACK,此时客户端的状态转换为FIN_WAIT_2;
第三次挥手,服务器端发送FIN请求,此时服务器端的状态转换为LAST_ACK;
第四次挥手,客户端接收到FIN请求,此时客户端的状态转换为TIME_WAIT,并回给服务器端ACK。
黑线为特殊情况下的状态转换,红线为客户端状态转换,绿线为服务器端状态转换。
TIME_WAIT定时经过两倍报文寿命后才会结束:
从程序的角度,可以使用 API 来控制实现半连接状态:
#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd: 需要关闭的socket的描述符
how: 允许为shutdown操作选择以下几种方式:
SHUT_RD(0):关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
SHUT_WR(1):关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发 出写操作。
SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以 SHUT_WR。
使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为 0 时才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。
注意:
端口复用最常用的用途是:
1.防止服务器重启时之前绑定的端口还未释放
2.程序突然退出而系统没有释放端口
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
参数:
- sickfd:要操作的文件描述符
- leevel:级别 -SOL_SOCKET(端口复用的级别)
- optname:选项的名称
-SO_REUSEADDR
-SO_REUSEPORT
-optval:端口复用的值(整型)
-1:可以复用
-0:不可以复用
-optlen:optval参数的大小
注:端口复用,设置的时机是在服务器绑定之前。
查看网络相关信息的命令:
netstat
参数: -a 所有socket
-p 显示正在使用socket的程序的名称
-n 直接使用IP地址,而不通过域名服务器
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。