目录
三次握手和四次挥手
三次握手: 建立连接需要三次握手过程
- 四次挥手: 断开连接需要四次挥手过程
- TCP是面向连接的安全的数据传输
- 在客户端与服务端建立建立的时候要经过三次握手的过程,在客户端与服务端断开连接的时候要经历四次挥手的过程,下图是客户端与服务端三次握手建立连接, 数据传输和断开连接四次挥手的全过程。
- TCP时序:
- 图的含义
- SYN:表示请求
- ACK:表示确认
- mss:表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。
- 服务端发送的SYN和客户端发送的SYN本身也会占1位
- 上图中ACK表示确认序号,确认序号的值是对方发送的序号值+数据的长度,
- 特别注意的是SYN和FIN本身也会占用一位
- 注:
- SYS----->synchronous
- ACK----->acknowledgement
- FIN------>finish
- 三次握手和四次挥手的过程都是在内核实现的
TCP数据报格式
窗口大小: 指的是缓冲区大小
- 通信的时候不再需要SYN标识位了, 只有在请求连接的时候需要SYN标识位
- 传输数据的时候的随机序号seq就是最近一次对方发送给自己的ACK的随机序号值, 而发给对方的ACK就是上次刚刚发给对方的ACK的值.
- 图中发送的ACK确认包表示给对方发送数据的一个确认, 表示你发送的数据我都收到了, 同时告诉对方下次发送该序号开始的数据
- 由于每次发送数据都会收到对方发来的确认包, 所以可以确认对方是否收到了, 若没有收到对方发来的确认包, 则会进行重发
- mss: 最大报文长度, 告诉对方我这边最多一次能收多少, 你不能超过这个长度
- win: 表示告诉对方我这边缓存大小最大是多少
滑动窗口(TCP流量控制)
- 主要作用: 滑动窗口主要是进行流量控制的
- 见下图:如果发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会导致接收缓冲区满而丢失数据。TCP协议通过“滑动窗口(Sliding Window)”机制解决这一问题。
- 发送端发起连接,声明最大段尺寸是1460,初始序号是0,窗口大小是4K,表示“我的接收缓冲区还有4K字节空闲,你发的数据不要超过4K”。接收端应答连接请求,声明最大段尺寸是1024,初始序号是8000,窗口大小是6K。发送端应答,三方握手结束。
- 发送端发出段4-9,每个段带1K的数据,发送端根据窗口大小知道接收端的缓冲区满了,因此停止发送数据。
- 接收端的应用程序提走2K数据,接收缓冲区又有了2K空闲,接收端发出段10,在应答已收到6K数据的同时声明窗口大小为2K。
- 接收端的应用程序又提走2K数据,接收缓冲区有4K空闲,接收端发出段11,重新声明窗口大小为4K。
- 发送端发出段12-13,每个段带2K数据,段13同时还包含FIN位。
- 接收端应答接收到的2K数据(6145-8192),再加上FIN位占一个序号8193,因此应答序号是8194,连接处于半关闭状态,接收端同时声明窗口大小为2K。
- 接收端的应用程序提走2K数据,接收端重新声明窗口大小为4K。
- 接收端的应用程序提走剩下的2K数据,接收缓冲区全空,接收端重新声明窗口大小为6K。
- 接收端的应用程序在提走全部数据后,决定关闭连接,发出段17包含FIN位,发送端应答,连接完全关闭。
- 上图在接收端用小方块表示1K数据,实心的小方块表示已接收到的数据,虚线框表示接收缓冲区,因此套在虚线框中的空心小方块表示窗口大小,从图中可以看出,随着应用程序提走数据,虚线框是向右滑动的,因此称为滑动窗口。
- 从这个例子还可以看出,发送端是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),在底层通讯中这些数据可能被拆成很多数据包来发送,但是一个数据包有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
- 图中win表示告诉对方我这边缓冲区大小是多少, mss表示告诉对方我这边最多一次可以接收多少数据, 你最好不要超过这个长度.
- 在客户端给服务端发包的时候, 不一定是非要等到服务端返回响应包, 由于客户端知道服务端的窗口大小, 所以可以持续多次发送, 当发送数据达到对方窗口大小了就不再发送, 需要等到对方进行处理, 对方处理之后可继续发送.
mss和MTU
- MTU:最大传输单元
- MTU:通信术语最大传输单元(Maximum Transmission Unit,MTU)
- 是指一种通信协议的某一层上面所能通过的最大数据包大小(以字节为 单位).
- 最大传输单元这个参数通常与通信接口有关(网络接口卡、串 口等), 这个值如果设置为太大会导致丢包重传的时候重传的数据量较大, 图中的最大值是1500, 其实是一个经验值.
- mss: 最大报文长度, 只是在建立连接的时候, 告诉对方我最大能够接收多少数据, 在数据通信的过程中就没有mss了.
函数封装思想
- 函数封装的思想-处理异常情况
- 结合man-page和errno进行封装.
- 在封装的时候起名可以把第一个函数名的字母大写, 如socket可以封装成Socket, 这样可以按shift+k进行搜索, shift+k搜索函数说明的时候不区分大小写, 使用man page也可以查看, man page对大小写不区分.
- 像accept,read这样的能够引起阻塞的函数
- 若被信号打断,由于信号的优先级较高, 会优先处理信号, 信号处理完成后,会使accept或者read解除阻塞, 然后返回, 此时返回值为 -1,设置errno=EINTR;
- errno=ECONNABORTED表示连接被打断,异常.
- errno宏
- 在/usr/include/asm-generic/errno.h文件中包含了errno所有的宏和对应的错误描述信息.
- wrap.h
#ifndef __WRAP_H_ #define __WRAP_H_ #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #include <strings.h> void perr_exit(const char *s); int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr); int Bind(int fd, const struct sockaddr *sa, socklen_t salen); int Connect(int fd, const struct sockaddr *sa, socklen_t salen); int Listen(int fd, int backlog); int Socket(int family, int type, int protocol); ssize_t Read(int fd, void *ptr, size_t nbytes); ssize_t Write(int fd, const void *ptr, size_t nbytes); int Close(int fd); ssize_t Readn(int fd, void *vptr, size_t n); ssize_t Writen(int fd, const void *vptr, size_t n); ssize_t my_read(int fd, char *ptr); ssize_t Readline(int fd, void *vptr, size_t maxlen); int tcp4bind(short port,const char *IP); #endif
- wrap.c
#include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #include <strings.h> void perr_exit(const char *s) { perror(s); exit(-1); } int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr) { int n; again: if ((n = accept(fd, sa, salenptr)) < 0) { if ((errno == ECONNABORTED) || (errno == EINTR)) goto again; else perr_exit("accept error"); } return n; } int Bind(int fd, const struct sockaddr *sa, socklen_t salen) { int n; if ((n = bind(fd, sa, salen)) < 0) perr_exit("bind error"); return n; } int Connect(int fd, const struct sockaddr *sa, socklen_t salen) { int n; if ((n = connect(fd, sa, salen)) < 0) perr_exit("connect error"); return n; } int Listen(int fd, int backlog) { int n; if ((n = listen(fd, backlog)) < 0) perr_exit("listen error"); return n; } int Socket(int family, int type, int protocol) { int n; if ((n = socket(family, type, protocol)) < 0) perr_exit("socket error"); return n; } ssize_t Read(int fd, void *ptr, size_t nbytes) { ssize_t n; again: if ( (n = read(fd, ptr, nbytes)) == -1) { if (errno == EINTR) goto again; else return -1; } return n; } ssize_t Write(int fd, const void *ptr, size_t nbytes) { ssize_t n; again: if ( (n = write(fd, ptr, nbytes)) == -1) { if (errno == EINTR) goto again; else return -1; } return n; } int Close(int fd) { int n; if ((n = close(fd)) == -1) perr_exit("close error"); return n; } /*参三: 应该读取的字节数*/ ssize_t Readn(int fd, void *vptr, size_t n) { size_t nleft; //usigned int 剩余未读取的字节数 ssize_t nread; //int 实际读到的字节数 char *ptr; ptr = vptr; nleft = n; while (nleft > 0) { if ((nread = read(fd, ptr, nleft)) < 0) { if (errno == EINTR) nread = 0; else return -1; } else if (nread == 0) break; nleft -= nread; ptr += nread; } return n - nleft; } ssize_t Writen(int fd, const void *vptr, size_t n) { size_t nleft; ssize_t nwritten; const char *ptr; ptr = vptr; nleft = n; while (nleft > 0) { if ( (nwritten = write(fd, ptr, nleft)) <= 0) { if (nwritten < 0 && errno == EINTR) nwritten = 0; else return -1; } nleft -= nwritten; ptr += nwritten; } return n; } static ssize_t my_read(int fd, char *ptr) { static int read_cnt; static char *read_ptr; static char read_buf[100]; if (read_cnt <= 0) { again: if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) { if (errno == EINTR) goto again; return -1; } else if (read_cnt == 0) return 0; read_ptr = read_buf; } read_cnt--; *ptr = *read_ptr++; return 1; } ssize_t Readline(int fd, void *vptr, size_t maxlen) { ssize_t n, rc; char c, *ptr; ptr = vptr; for (n = 1; n < maxlen; n++) { if ( (rc = my_read(fd, &c)) == 1) { *ptr++ = c; if (c == '\n') break; } else if (rc == 0) { *ptr = 0; return n - 1; } else return -1; } *ptr = 0; return n; } int tcp4bind(short port,const char *IP) { struct sockaddr_in serv_addr; int lfd = Socket(AF_INET,SOCK_STREAM,0); bzero(&serv_addr,sizeof(serv_addr)); if(IP == NULL){ //如果这样使用 0.0.0.0,任意ip将可以连接 serv_addr.sin_addr.s_addr = INADDR_ANY; }else{ if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0){ perror(IP);//转换失败 exit(1); } } serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(port); Bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)); return lfd; }
粘包的概念
- 粘包: 多次数据发送, 收尾相连, 接收端接收的时候不能正确区分第一次发送多少, 第二次发送多少.
- 粘包问题分析和解决?
- 方案1: 包头+数据
- 如4位的数据长度+数据 -----------> 00101234567890
- 其中0010表示数据长度, 1234567890表示10个字节长度的数据.
- 另外, 发送端和接收端可以协商更为复杂的报文结构, 这个报文结构就相当于双方约定的一个协议.
- 方案2: 添加结尾标记.
- 如结尾最后一个字符为\n \$等.
- 方案3: 数据包定长
- 如发送方和接收方约定, 每次只发送128个字节的内容, 接收方接收定长128个字节就可以了.
高并发服务器
- 如何支持多个客户端---支持多并发的服务器
- 由于accept和read函数都会阻塞, 如当read的时候, 不能调用accept接受新的连接, 当accept阻塞等待的时候不能read读数据.
- 方案: 使用多进程, 可以让父进程接受新连接, 让子进程处理与客户端通信
- 思路: 让父进程accept接受新连接, 然后fork子进程, 让子进程处理通信, 子进程处理完成后退出, 父进程使用SIGCHLD信号回收子进程.
- 处理流程:
1 创建socket, 得到一个监听的文件描述符lfd---socket() 2 将lfd和IP和端口port进行绑定-----bind(); 3 设置监听----listen() 4 进入while(1) { //等待有新的客户端连接到来 cfd = accept(); //fork一个子进程, 让子进程去处理数据 pid = fork(); if(pid<0) { exit(-1); } else if(pid>0) { //关闭通信文件描述符cfd close(cfd); } else if(pid==0) { //关闭监听文件描述符 close(lfd); //收发数据 while(1) { //读数据 n = read(cfd, buf, sizeof(buf)); if(n<=0) { break; } //发送数据给对方 write(cfd, buf, n); } close(cfd); //下面的exit必须有, 防止子进程再去创建子进程 exit(0); } } close(lfd);
- 还需要添加的功能: 父进程使用SIGCHLD信号完成对子进程的回收
- 注意点: accept或者read函数是阻塞函数, 会被信号打断, 此时不应该视为一个错误,errno=EINTR
示例代码
//多进程版本的网络服务器 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #include <ctype.h> #include "wrap.h" int main() { //创建socket int lfd = Socket(AF_INET, SOCK_STREAM, 0); //绑定 struct sockaddr_in serv; bzero(&serv, sizeof(serv)); serv.sin_family = AF_INET; serv.sin_port = htons(8888); serv.sin_addr.s_addr = htonl(INADDR_ANY); Bind(lfd, (struct sockaddr *)&serv, sizeof(serv)); //设置监听 Listen(lfd, 128); pid_t pid; int cfd; char sIP[16]; socklen_t len; struct sockaddr_in client; while(1) { //接受新的连接 len = sizeof(client); memset(sIP, 0x00, sizeof(sIP)); cfd = Accept(lfd, (struct sockaddr *)&client, &len); printf("client:[%s] [%d]\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, sIP, sizeof(sIP)), ntohs(client.sin_port)); //接受一个新的连接, 创建一个子进程,让子进程完成数据的收发操作 pid = fork(); if(pid<0) { perror("fork error"); exit(-1); } else if(pid>0) { //关闭通信文件描述符cfd close(cfd); } else if(pid==0) { //关闭监听文件描述符 close(lfd); int i=0; int n; char buf[1024]; while(1) { //读数据 n = Read(cfd, buf, sizeof(buf)); if(n<=0) { printf("read error or client closed, n==[%d]\n", n); break; } //printf("client:[%s] [%d]\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, sIP, sizeof(sIP)), ntohs(client.sin_port)); printf("[%d]---->:n==[%d], buf==[%s]\n", ntohs(client.sin_port), n, buf); //将小写转换为大写 for(i=0; i<n; i++) { buf[i] = toupper(buf[i]); } //发送数据 Write(cfd, buf, n); } //关闭cfd close(cfd); exit(0); } } //关闭监听文件描述符 close(lfd); return 0; }
- 方案: 使用多线程
- 让主线程接受新连接, 让子线程处理与客户端通信; 使用多线程要将线程设置为分离属性, 让线程在退出之后自己回收资源.
- 多线程版本的服务器开发流程
{ 1 创建socket, 得到一个监听的文件描述符lfd---socket() 2 将lfd和IP和端口port进行绑定-----bind(); 3 设置监听----listen() 4 while(1) { //接受新的客户端连接请求 cfd = accept(); //创建一个子线程 pthread_create(&threadID, NULL, thread_work, &cfd); //设置线程为分离属性 pthread_detach(threadID); } close(lfd); } 子线程执行函数: void *thread_work(void *arg) { //获得参数: 通信文件描述符 int cfd = *(int *)arg; while(1) { //读数据 n = read(cfd, buf, sizeof(buf)); if(n<=0) { break; } //发送数据 write(cfd, buf, n); } close(cfd); }
- 子线程能否关闭lfd?
- 子线程不能关闭监听文件描述符lfd,原因是子线程和主线程共享文件描述符,而不是复制的.
- 主线程能否关闭cfd?
- 主线程不能关闭cfd, 主线程和子线程共享一个cfd, 而不是复制的, close之后cfd就会被真正关闭.
- 多个子线程共享cfd,,导致cfd是最后一次连接的值,前面的值被覆盖,解决思路
struct INFO { int cfd; pthread_t threadID; struct sockaddr_in client; }; struct INFO info[100]; //初始化INFO数组 for(i=0; i<100; i++) { info[i].cfd=-1; } for(i=0; i<100; i++) { if(info[i].cfd==-1) { //这块内存可以使用 } } if(i==100) { //拒绝接受新的连接 close(cfd); }
示例代码
//多线程版本的高并发服务器 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #include <ctype.h> #include <pthread.h> #include "wrap.h" //子线程回调函数 void *thread_work(void *arg) { int cfd = *(int *)arg; printf("cfd==[%d]\n", cfd); int i; int n; char buf[1024]; while(1) { //read数据 memset(buf, 0x00, sizeof(buf)); n = Read(cfd, buf, sizeof(buf)); if(n<=0) { printf("read error or client closed,n==[%d]\n", n); break; } printf("n==[%d], buf==[%s]\n", n, buf); for(i=0; i<n; i++) { buf[i] = toupper(buf[i]); } //发送数据给客户端 Write(cfd, buf, n); } //关闭通信文件描述符 close(cfd); pthread_exit(NULL); } int main() { //创建socket int lfd = Socket(AF_INET, SOCK_STREAM, 0); //设置端口复用 int opt = 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int)); //绑定 struct sockaddr_in serv; bzero(&serv, sizeof(serv)); serv.sin_family = AF_INET; serv.sin_port = htons(8888); serv.sin_addr.s_addr = htonl(INADDR_ANY); Bind(lfd, (struct sockaddr *)&serv, sizeof(serv)); //设置监听 Listen(lfd, 128); int cfd; pthread_t threadID; while(1) { //接受新的连接 cfd = Accept(lfd, NULL, NULL); //创建子线程 pthread_create(&threadID, NULL, thread_work, &cfd); //设置子线程为分离属性 pthread_detach(threadID); } //关闭监听文件描述符 close(lfd); return 0; }
- 【注】:参考黑马linux C++教程