一、多进程实现并发
思路:
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 rechild(int arg){ while(1){ int ret = waitpid(-1, NULL, WNOHANG); //还有子进程活着 if(ret==0){ break; } else if(ret==-1){ //所有子进程都被回收 break; }else{ printf("被回收pid: %d\n", ret); } } } int main(){ //为了回收子进程资源,wait和waitpid显得不够合适,需要利用子进程结束时发送给父进程的信号进行回收 //注册信号捕捉 struct sigaction act; act.sa_flags=0; sigemptyset(&act.sa_mask); act.sa_handler = rechild; sigaction(SIGCHLD,&act,NULL); //1.创建套接字 int lfd = socket(AF_INET, SOCK_STREAM, 0); if(lfd==-1){ perror("socket"); exit(0); } //2.绑定 struct sockaddr_in sock_addr; sock_addr.sin_family = AF_INET; sock_addr.sin_port = htons(9999); //服务器端可以为0=INADDR_ANY sock_addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(lfd, (struct sockaddr*)&sock_addr, sizeof(sock_addr)); if(ret==-1){ perror("bind"); exit(0); } //3.监听 ret = listen(lfd, 128); if(ret==-1){ perror("listen"); exit(0); } //4.循环等待客户端连接 while(1){ //接收连接 struct sockaddr_in cli_addr; //定义长度方便取地址 int len = sizeof(cli_addr); int cfd = accept(lfd, (struct sockaddr*)&cli_addr, &len); if(cfd==-1){ if(errno = EINTR){ continue; } perror("accept"); exit(0); } //给每一个连接创建子进程,和客户端进行通信 pid_t pid = fork(); if(pid==0){ //子进程 //获取客户端信息 char cli_IP[16]; //获取IP inet_ntop(AF_INET, &cli_addr.sin_addr.s_addr, cli_IP, sizeof(cli_IP)); //获取端口号 unsigned short cli_port = ntohs(cli_addr.sin_port); printf("Client IP is : %s, port number is :%d\n", cli_IP, cli_port); //先接收客户端发送的数据 char recBuf[1024]; while(1){ int num = read(cfd, &recBuf, sizeof(recBuf)); if(num==-1){ perror("read"); exit(0); } else if(num>0){ printf("recv from client: %s\n", recBuf); }else{ printf("Client is closed/.....\n"); break; } //回射服务器 write(cfd, recBuf, sizeof(recBuf)); } close(cfd); //退出当前子进程 exit(0); } } 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]; char wrbuf[20]; while(1){ //3.给服务器端发送数据 fgets(wrbuf, 20, stdin); write(fd, wrbuf, sizeof(wrbuf)+1); //4.读取服务器端的数据 sleep(1); int len = read(fd, rebuf, sizeof(rebuf)+1); 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; }
二、多线程实现并发
思路大致与多进程相同,但是要注意的是,线程执行函数中需要的参数,最好封装成结构体指针进行传递,其中存储结构体变量值的变量应该是全局变量或者是其他不存在栈中的变量,不然当局部变量销毁以后,所传地址也就没有了意义,会导致出现错误。
服务器端:
/* 多线程实现 */ #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 cli_addr;//客户端信息 pthread_t tid;//线程号 }; //结构体大小,可供同时连接的数量,是为了避免局部变量被销毁(因为线程栈不共享) struct sockInfo sockinfos[128]; void * working(void * arg){ //子线程和客户端通信 需要得到cfd, 客户端的信息, 线程号。全部封装到结构体中 struct sockInfo* pinfo = (struct sockInfo*) arg; char cli_IP[16]; //获取IP inet_ntop(AF_INET, &pinfo->cli_addr.sin_addr.s_addr, cli_IP, sizeof(cli_IP)); //获取端口号 unsigned short cli_port = ntohs(pinfo->cli_addr.sin_port); printf("Client IP is : %s, port number is :%d\n", cli_IP, cli_port); //先接收客户端发送的数据 char recBuf[1024]; while(1){ int num = read(pinfo->fd, &recBuf, sizeof(recBuf)); if(num==-1){ perror("read"); exit(0); } else if(num>0){ printf("recv from client: %s\n", recBuf); }else{ printf("Client is closed/.....\n"); break; } //回射服务器 write(pinfo->fd, recBuf, strlen(recBuf)+1); } close(pinfo->fd); pinfo->tid=-1; return NULL; } int main(){ //1.创建套接字 int lfd = socket(AF_INET, SOCK_STREAM, 0); if(lfd==-1){ perror("socket"); exit(0); } //2.绑定 struct sockaddr_in sock_addr; sock_addr.sin_family = AF_INET; sock_addr.sin_port = htons(9999); //服务器端可以为0=INADDR_ANY sock_addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(lfd, (struct sockaddr*)&sock_addr, sizeof(sock_addr)); if(ret==-1){ perror("bind"); exit(0); } //3.监听 ret = listen(lfd, 128); if(ret==-1){ perror("listen"); exit(0); } //初始化数据 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 cli_addr; //定义长度方便取地址 int len = sizeof(cli_addr); int cfd = accept(lfd, (struct sockaddr*)&cli_addr, &len); struct sockInfo * pinfo; //从数组中找到一个可用的元素 for(int i=0;i<max;++i){ if(sockinfos[i].tid==-1){ pinfo=&sockinfos[i]; break; } if(i==max-1){ sleep(1); i=0; } } pinfo->fd = cfd; memcpy(&pinfo->cli_addr, &cli_addr, len); //创建子线程 pthread_create(&pinfo->tid, NULL, working, pinfo); //设置线程分离 pthread_detach(pinfo->tid); } close(lfd); return 0; }
客户端代码同多进程。
三、TCP通信状态的改变
两倍最大报文寿命:
2MSL(Maximum Segment Lifetime)主动断开连接的一方, 最后进出入一个 TIME_WAIT状态, 这个状态会持续: 2msl
msl: 官方建议: 2分钟, (Linux)实际是30s
当 TCP 连接主动关闭方接收到被动关闭方发送的 FIN 和最终的 ACK 后,连接的主动关闭方必须处于TIME_WAIT 状态并持续 2MSL 时间。这样就能够让 TCP 连接的主动关闭方在它发送的 ACK 丢失的情况下重新发送最终的 ACK。主动关闭方重新发送的最终 ACK 并不是因为被动关闭方重传了 ACK(它们并不消耗序列号,被动关闭方也不会重传),而是因为被动关闭方重传了它的 FIN。事实上,被动关闭方总是重传 FIN 直到它收到一个最终的 ACK。
半关闭:
当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发送的数据,但是 A 已经不能再向 B 发送数据。
从程序的角度,可以使用 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. 如果有多个进程共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用进程都调用了 close,套接字将被释放。
2. 在多进程中如果一个进程调用了 shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法进行通信。但如果一个进程 close(sfd) 将不会影响到其它进程。
四、端口复用
首先是查看常用网络相关信息的命令
netstat
参数:
-a 所有的socket
-p 显示正在使用socket的程序名称
-n 直接使用IP地址,而不通过域名服务器
端口复用最常用的用途是:
防止服务器重启时之前绑定的端口还未释放
程序突然退出而系统没有释放端口
#include <arpa/inet.h> 只能用于套接字 int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen); 参数:(端口复用下) -sockfd:需要设置的文件描述符 -level:级别 -SOL_SOCKET (端口复用的级别) -optname:选项的名称 -SO_REUSEADDR -SO_REUSEPORT -optval:端口复用的值(整形) -1:可以复用 -0:不可以复用 -optlen:optval参数大小 端口复用的设置时机是在服务器绑定端口之前,即bind()前
下面是level参数和optname,optval参数的全部值: