alin的学习之路(Linux网络编程:二)(三次握手四次挥手、read函数返回值、错误函数封装、多进程高并发服务器)
1. 服务器获取客户端地址和端口号
accept函数会返回客户端的
sockaddr
,通过使用
inet_ntop()
和
ntohs()
即可获取客户端地址和端口号
char clt_IP[1024];clt_addr_len = sizeof(clt_addr);cfd = Accept(lfd,(struct sockaddr*)&clt_addr,&clt_addr_len);printf(\"客户端ip:%s,port:%d接入服务器\\n\",inet_ntop(AF_INET,&clt_addr.sin_addr.s_addr,clt_IP,sizeof(clt_IP)),ntohs(clt_addr.sin_port));
2. TCP 通信时序
1. 三次握手
- 注意:三次握手由客户端发出请求,而非服务器
-
主动发起连接请求端(客户端),发送 SYN 标志位,携带 序号
-
被动接收连接请求端(服务端),回复 ACK 标志位,携带 确认序号,并发送 SYN 标志位,携带序号
-
主动发起连接请求端(客户端),发送 ACK 标志位
—— 当第二个 ACK 发送完成, 标志 3次握手完成。 连接建立成功。
—— 服务器, accept() 成功返回。
—— 客户端, connect() 成功返回。
SYN和序号 + ACK和确认序号表示该序号之前的数据包全部接收到,该序号和确认序号也就是TCP通信的安全之处
2. 四次挥手
- 主动关闭连接请求端(客户端),发送 FIN 标志位 , 携带序号。
- 被动关闭连接请求端(服务端),接收 FIN, 发送 ACK 标志位,携带 确认序号。
—— 半关闭完成。 - 被动关闭连接请求端(服务端),发送 FIN 标志位 , 携带序号。
- 主动关闭连接请求端(客户端),接收 FIN, 发送 ACK 标志位,携带 确认序号。
—— 最后一个 ACK 被 收到, 标志着 4次挥手完成。 TCP 连接关闭。
3. 半关闭
半关闭的实现, 依赖底层内核实现 socket 的原理。
半关闭关闭的是socket缓冲区,也就是关闭了数据的发送,但标志位等在TCP数据报格式的前面
4. 滑动窗口
通知通信的对端, 本端缓冲区的剩余空间大小(实时), 保证数据不会丢失。
滑动窗口 在 TCP 协议格式中存储, 上限 为 65536
3. read函数的返回值
- > 0 实际读到的字节数。
- = 0 已经读到结尾。(对端关闭)【重点】
- -1 应该进一步判断 errno 的值。
errno == EAGAIN or EWOULDBLOCK : 设置了非阻塞方式读,但没有数据。
errno == EINTR : 慢速系统调用,被信号中断。
errno == “其他情况”。 异常。
4. 错误处理函数封装
因为在socket编程中,每一个函数的调用都需要检查返回值,并且所有的函数都加上返回值后使得代码变得冗余。
解决办法:可以自行封装函数,将函数名写为首字母大写,这样在vim中还可以使用K进行跳转
- wrap.c存放 网络通信常用的 自定义函数实现。
- 命名方式: 系统调用函数首字母大写, 方便跳转 man 手册。
- 函数功能,添加系统调用的 出错场景。
- 在 server.c / client.c 中, 调用自定义函数。
server.c 和 wrap.c 编译生成 server
client.c 和 wrap.c 编译生成 client
存放 网络通信常用的 自定义函数原型。
wrap.c
#include \"wrap.h\"void sys_err(const char* str){perror(str);exit(1);}int Socket(int domain, int type, int protocol){int n = 0;n = socket(domain,type,protocol);if(n < 0)sys_err(\"socket error\");return n;}int Accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen){int n = 0;again:n = accept(sockfd,addr,addrlen);if(n < 0){if(errno == EINTR || errno == ECONNABORTED)goto again;elsesys_err(\"accept error\");}return n;}int Bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen){int n = 0;n = bind(sockfd,addr,addrlen);if(n < 0)sys_err(\"bind error\");return n;}int Listen(int sockfd, int backlog){int n = 0;n = listen(sockfd,backlog);if(n < 0)sys_err(\"listen error\");return n;}int Connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen){int n = 0;n = connect(sockfd,addr,addrlen);if(n < 0)sys_err(\"connect error\");return n;}ssize_t Read(int fd, void *buf, size_t count){ssize_t n = 0;again:n = read(fd,buf,count);if(n < 0){if(errno == EINTR)goto again;elsesys_err(\"read error\");}return n;}ssize_t Write(int fd, const void *buf, size_t count){ssize_t n = 0;again:n = write(fd,buf,count);if(n < 0){if(errno == EINTR)goto again;elsesys_err(\"write error\");}return n;}int Close(int fd){int n = 0;n = close(fd);if(n < 0)sys_err(\"close error\");return n;}ssize_t Readn(int fd, void *vptr, size_t n){ssize_t nread;ssize_t nleft = n;char* ptr = (char*)vptr;while(nleft > 0){nread = read(fd,ptr,nleft);if(nread < 0){if(errno == EINTR)nread = 0;elsereturn -1;}else if(nread == 0)break;nleft -= nread;ptr += nread;}return n-nleft;}ssize_t Writen(int fd, const void *vptr, size_t n){ssize_t nwrite;ssize_t nleft = n;char* ptr = (char*)vptr;while(nleft > 0){nwrite = write(fd,ptr,nleft);if(nwrite < 0){if(errno == EINTR)nwrite = 0;elsereturn -1;}else if(nwrite == 0)break;nleft -= nwrite;ptr += nwrite;}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:read_cnt = read(fd,read_buf,sizeof(read_buf));if(read_cnt == -1){if(errno == EINTR)goto again;elsereturn -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 = (char*)vptr;for(n=1 ;n<maxlen ;++n){rc = my_read(fd,&c);if(rc == 1){*ptr++ = c;if(c == \'\\n\')break;}else if(rc == 0){*ptr = 0;return n-1;}else if(rc == -1)return -1;}*ptr = 0;return n;}
5. 高并发服务器(多进程)
1. 程序流程
-
Socket函数创建监听套接字
-
Bind函数绑定IP地址和端口号
-
Listen函数设置最大监听数
-
while(1){
-
Accept函数阻塞等待客户端接入,创建通信套接字
-
fork创建子进程
子进程:
Close关闭监听套接字文件描述符
- Read函数接收客户端发送的数据(当Read的返回值为0的时候,Close通信套接字文件描述符,子进程退出)
- 小写转大写
- Write函数将处理后的数据发送给客户端
- Close关闭通信套接字文件描述符
父进程:
- 关闭通信套接字文件描述
- 注册 SIGCHLD 的信号捕捉函数,用来回收子进程
- continue 继续回到循环,等待新的客户端接入
}
2. 代码实现
#include <ctype.h>#include \"wrap.h\"#include <sys/wait.h>#include <signal.h>#define SRV_PORT 9999void func(int signo){while(1){int ret = waitpid(-1,NULL,0);if(ret == -1)break;}return ;}int main(){int lfd,cfd;socklen_t clt_addr_len;pid_t pid;char clt_IP[1024];char buf[BUFSIZ] = {0};struct sockaddr_in srv_addr,clt_addr;srv_addr.sin_family = AF_INET;srv_addr.sin_port = htons(SRV_PORT);srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);lfd = Socket(AF_INET,SOCK_STREAM,0);Bind(lfd,(struct sockaddr*)&srv_addr,sizeof(srv_addr));Listen(lfd,128);char SRV_IP[1024];printf(\"服务器已开启ip:%s,port:%d\\n\",inet_ntop(AF_INET,&srv_addr.sin_addr.s_addr,SRV_IP,sizeof(SRV_IP)),ntohs(srv_addr.sin_port));while(1){clt_addr_len = sizeof(clt_addr);cfd = Accept(lfd,(struct sockaddr*)&clt_addr,&clt_addr_len);printf(\"客户端ip:%s,port:%d接入服务器\\n\",inet_ntop(AF_INET,&clt_addr.sin_addr.s_addr,clt_IP,sizeof(clt_IP)),ntohs(clt_addr.sin_port));pid = fork();if(-1 == pid){sys_err(\"fork error\");}else if(0 == pid){Close(lfd);break;}else{Close(cfd);struct sigaction act = {.sa_handler = func};int ret = sigaction(SIGCHLD,&act,NULL);if(-1 == ret){sys_err(\"sigaction error\");}continue;}}if(0 == pid){while(1){int ret = Read(cfd,buf,sizeof(buf));if(0 == ret){printf(\"客户端ip:%s,port:%d断开连接\\n\",clt_IP,ntohs(clt_addr.sin_port));break;}for(int i=0 ;i<ret ;++i){buf[i] = toupper(buf[i]);}Write(cfd,buf,ret);printf(\"发给ip:%s,port:%d数据:%s\",clt_IP,ntohs(clt_addr.sin_port),buf);}Close(cfd);}return 0;}
注意:accept函数的参数中传入传出参数是客户端的IP地址变量