socket网络编程基础
本节主要内容:
1. 协议的概念
2. b/s, c/s
b/s即browser/server体系结构,即浏览器/服务器端,如购物网站等
c/s即client/server体系结构,即客户端/服务器端,需要提前下载客户端,如现在用的很多软件,都是c/s模式
c/s 优点 1. 协议选用灵活 2. 数据可以提前缓存
缺点 1. 对用户安全构成威胁 2. 开发工作量大,调试困难
b/s 优点 1. 2. 3. 跨平台
缺点 1. http
3. 分层模型 7 4
计算机网络体系结构
七层模型口诀:物数网传会表应
- 网络层协议:IP:不稳定,硬件联系紧密
- 传输层协议:TCP/UDP
TCP三次握手
SYN,FIN,ACK都是标志位,这些标志位都在TCP协议中,发送数据包的时候一起发送过去
(1)为什么要进行三次握手
(2)三次握手是怎么进行的
SYN是同步位
三次握手的过程:
(1)首先由客户端发起连接请求,发送SYN报文(其中),并且发送seq=x
(2)服务器端接收到客户端发起的连接请求,AYN=1,ACK=1,并且发送ack报文ack=x+1,表示在x之前的数据包都已经收到了,并且发送自己的seq=y
(3)客户端接收到服务器端发送回的应答,ACK=1,seq=x+1,ack=y+1,表示在y之前的数据包都已经收到了。
(3)四次握手
断开连接时需要采用四次握手的方式,这与Linux操作系统中一端关闭连接,一端不关闭连接,这种状态称为半关闭
(1)当客户端想要先关闭连接时,首先发送一个FIIN标志位,并发送之前发送到的ack报文的值
4. 协议格式
数据包格式
在两个PC之间进行通信时,如果直接将所传输的内容丢到网络中即会导致另一个PC无法收到相应的数据,因此在进行通信之前,要对所传内容进行封装
在数据进行传输之前,需要一层一层对数据进行封装;在接收数据之前,需要一层一层对数据进行解封装,这两个过程都是操作系统完成的
因此刚刚封装好了的数据其实是需要分区的
数据要进行传输,必须进行封装
要清楚的一点是,数据一定是从网卡传输出去的,网卡要连接到你的网络,中途可能会通过很多个路由器。
通信过程
两台计算机通过TCP/IP协议通讯的过程如下:
当数据要在两个主机之间进行通信时,两个PC可能是位于不同网络之中,因此就需要依靠路由器来进行网络的传输,如下图所示
如上图所示,假如已经封装好的数据包需要从A主机传输到B主机,那么就需要通过很多个路由器,在这么多条可行路径中,依靠路由表来进行选路。
并且一旦主机A到主机B的路径已经建立好了,后续就会一直按照这个路径进行传输
例如:假如这里需要传输一本小说,其肯定是由很多个封装好的数据包组成的,当连接一旦建立后,后续的数据包就不需要再通过选路了,这就是TCP稳定的原因,因为其通过的路由器都是固定的。
在传输过程中,路由器寻路的依据:目的主机的IP无疑是一个很大的依据,但是其就是单纯依靠这样一个IP吗?显然不是的,还有一个重要依据,就是我们下面所要讲的以太网格式帧
以太网格式帧
首先介绍一下以太格式帧
由上图可以知道,以太网格式帧由6个字节的目的地址和源地址组成,特别注意,这里的目的地址和源地址是指网卡的硬件地址而不是IP地址,类型描述了上一层所使用的协议,是TCP还是什么协议,由于我们之前知道数据链路层主要是用来校验的,因此最后就是CRC是用来校验的
我们在linux系统中输入ifconfig中第一行就可以看到MAC地址,这个网卡是唯一的
那么当主机之间进行通信时,每个主机有其自己的MAC地址,路由器也有其自己的MAC地址,相互连通的物理设备之间怎么可以获取对方的MAC地址呢,这就是我们接下来要讲的ARP协议
ARP协议主要就是用来请求下一跳的MAC地址
由上可以看出,ARP占用的是最小字节,PAD表示填充
ARP数据报格式如下
对此我们进行如下解释
当刚开始的时候,主机像连接的路由发送ARP请求,由于目的MAC未知,因此将其填充为0,目的端IP也是知道的
然后进行ARP广播,与主机相连接的每一个物理设备都会受到这条信息,并且将接收端IP与自己的IP进行比较,若相同,则将自己的MAC地址进行填充,如上述过程所示。每个路由称为一跳。
整个传输过程简述
每个路由器只进行了部分解封装,即解封装链路层和网络层,根据路由表来进行选路
最长的箭头表示PC之间的通信,中间的小箭头表示路由器之间的通信。
ARP数据:用来获取下一跳的MAC地址
TTL:一跳单位,每经过一个路由器TTL-1,最长为56
ARPCs数据包格式
IP段格式
我们常见的IP地址如192.168.0.222--字符串格式
在编程过程中需要将其转化为数值(unsigned int)类型
IP段格式对应MAC地址的数据段
TCP/UDP
TCP在传输过程中也会出错,只是如果出错了还可以重新发送
传输层常见协议:tcp/udp
5. NAT映射 打洞机制
假设小明和小红利用QQ进行聊天,则他们之间按理说应该是这样的:
两者利用QQ进行聊天时,会出现如下情况:左端发送的数据通过NAT映射表将私网IP地址映射成公网IP地址,再利用路由器与腾讯服务器端建立通信,经由服务器将发送的数据包传送到右端。如果此时是传输文字还好,但是如果两者要进行视频电话,如果还是经由腾讯服务器就会使得传输次数过多,造成传输速度过慢,因此腾讯借助公网IP帮助左端的主机和右端的主机打个洞来提高数据传输的效率。
有个问题是为什么是腾讯帮助打洞?这是因为路由器的保护机制:对于陌生IP第一次发送过来的数据包,路由器会进行屏蔽或丢弃,这是为了防止网络的恶意攻击。
简单来说在我们登入QQ的时候,就会访问腾讯服务器,而腾讯服务器也会回一个数据包,这个数据包会携带腾讯服务器公网的IP,相对来说服务器的公网IP在A,B那里都是熟悉的IP(为了防止陌生IP被屏蔽),而服务器借助公网IP帮助A, B完成打洞(打洞就是实现一种通路),当它把这个洞打好以后A, B就可以实时通信。打洞是由服务器来完成的,最终的目的是为了提高数据传输的效率。
对于不同设备之间的通信,可以分为以下三类
公-公:设备之间直接进行通信
私-公:借助NAT映射表
私-私:借助NAT映射表和打洞机制,实现两者之间的直接通信。
6. 套接字
一、预备知识
利用socket可以唯一地找到一个进程
socket是linux文件的一个类型,伪文件(不占用实际地址)
其是一个全双工的,写入的同时也可以读出,能够实现双向通信而不导致写入的数据将读取的数据覆盖掉,socket采用的机制是:利用两个缓冲区,一个缓冲区用来读取,一个缓冲区用来写入。
socket总结
1. socket一般成对出现
2. socket需要绑定IP+端口号
3. 其具有两个缓冲区,一个读入,一个写出
- 网络字节序传输
1. 存储方法:
数据进行传输时一定需要转换成二进制字节流进行传输
主机采用小端法存储(低字节存低位,高字节存高位)
网络字节流采用大端法存储(低字节存高位,高字节存低位)
当在网络中进行发送数据时,由于需要不断的解包得到MAC地址和IP地址,但是主机和网络字节流采用的存储方法不同,那么怎么解决这个问题呢?
因此需要做网络字节序和主机字节序之间的转换。
2. sockaddr数据结构
原始数据结构是struct sockaddr,后来变成了struct sockaddr_in
struct sockaddr_in内部成员
主要的三个
addr.sin_family=AF_INET/AFINET6;
addr.sin_port=htons/ntohs;
addr.sin_addr.s_addr=htonl/ntohl;
3. socket中实现C/S常用函数
一共三个函数需要强转:bind(), accept(),connect()
(1)socket():目的:创建套接字
int socket(int domain, int types, int protocal)
types:常见的有两种,TCP/UDP
成功,返回指向新创建的socket文件描述符,失败返回-1
(2)bind函数
sockfd:文件描述符,就是刚刚socket函数返回的值
第二个参数:由于Bind是要绑定IP和端口号,这个IP和端口号都放在sockaddr_i这个结构体中
(3)listen():指定监听上限数,即同时允许多少客户端和其建立连接
即:排队建立3次握手队列和刚刚建立三次握手队列的链接数和
(4)accept()作用:接受函数,接受,在socket的基础上接受一个连接
产生新的文件描述符,指向客户端的socket即客户端利用connect()传进去的那个socket
*addr:客户端的addr
*addrlen:客户端的addr的长度,注意在实际上进行传值的时候,不能用sizeof(),因为其是传入传出函数
和客户端发送数据读取数据都是通过返回的新的文件描述符来进行的,重点是返回值的理解
accept返回的才是跟客户端简历连接的
(5)connect():建立连接
7. TCP C/S模型
6. client.c
说明:
1. 服务器端是必须要用bind()函数来进行IP地址和端口号的绑定的,但是客户端不需要调用bind()函数,因为当不调用Bind()函数时,会自动给socket分配一个IP和端口号,但是服务器不绑定的话,语法上是没有问题的,但服务器总是变会出问题
7.半关闭状态
8. 端口复用
在server的TCP连接没有完全断开之前不允许重新监听是不合理的,因为TCP连接没有完全断开指的是connfd没有完全断开,而我们重新监听的是lis-tenfd,虽然占用的是同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address,解决这个问题的方法是使用setsocket()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符
8. TCP通信机制
其与程序对应关系如下
10 客户端服务器程序
10.1 一对一的客户端服务器程序
顾名思义,我们所写的服务器程序一次只能响应一个客户端的程序。
- 客户端程序:server.c,此客户端程序按照socket(),bind(),listen(),accept(),read(),write()顺序编写即可
1 #include <stdio.h> 2 #include <sys/socket.h> 3 #include <stdlib.h> 4 #include <arpa/inet.h> 5 #include <ctype.h> 6 #include <unistd.h> 7 #include <string.h> 8 9 #define SERV_PORT 5555 10 #define SERV_IP "127.0.0.1" 11 #define CLIENT_MAX 128 12 #define BUFSIZE 128 13 14 void perr_exit(char *s) 15 { 16 perror(s); 17 exit(1); 18 } 19 20 21 int main() 22 { 23 int listenfd,connfd; 24 struct sockaddr_in servaddr,cliaddr; 25 int i=0; 26 char buf[BUFSIZE],clie_IP[BUFSIZE]; 27 //create the socket; 28 29 listenfd=socket(AF_INET,SOCK_STREAM,0); 30 if(listenfd==-1) 31 perr_exit("create the socket error!\n"); 32 bzero(&serv_addr,sizeof(serv_addr)); 33 servaddr.sin_family=AF_INET; 34 servaddr.sin_port=htons(SERV_PORT); 35 inet_pton(AF_INET,SERV_IP,&servaddr.sin_addr.s_addr); 36 37 //bind the IP and port; 38 bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)); 39 40 //figure out maxinum of clients 41 listen(listenfd,CLIENT_MAX); 42 43 socklen_t clilen=sizeof(cliaddr); 44 connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&clilen); 45 if(connfd<0) 46 perr_exit("accept error!"); 47 else 48 printf("client IP:%s,client port:%d\n", 49 inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr),clie_IP,sizeof(clie_IP)), 50 ntohs(cliaddr.sin_port)); 51 52 while(1) 53 { 54 int n=read(connfd,buf,sizeof(buf)); 55 if(n<0) 56 perr_exit("read error!\n"); 57 for(i=0;i<n;++i) 58 buf[i]=toupper(buf[i]); 59 write(connfd,buf,n); 60 } 61 close(listenfd); 62 close(connfd); 63 return 0; 64 }
- 服务端程序:client.c
1 #include <stdio.h> 2 #include <sys/socket.h> 3 #include <stdlib.h> 4 #include <arpa/inet.h> 5 #include <ctype.h> 6 #include <unistd.h> 7 #include <string.h> 8 9 #define SERV_PORT 6000 10 #define SERV_IP "127.0.0.5" 11 #define BUFSIZE 1024 12 13 void perr_exit(const char *s) 14 { 15 perror(s); 16 exit(-1); 17 } 18 19 int main() 20 { 21 int connfd,n,ret; 22 struct sockaddr_in servaddr; 23 char buf[BUFSIZE]; 24 connfd=socket(AF_INET,SOCK_STREAM,0); 25 if(connfd==-1) 26 perr_exit("create the socket error!"); 27 28 //initialize the pointer 29 memset(&servaddr,0,sizeof(servaddr)); 30 servaddr.sin_family=AF_INET; 31 servaddr.sin_port=htons(SERV_PORT); 32 //servaddr.sin_addr.s_addr=htonl(INADDR_ANY); 33 inet_pton(AF_INET,SERV_IP,&servaddr.sin_addr.s_addr); 34 35 //connect the server 36 socklen_t servlen=sizeof(servaddr); 37 ret=connect(connfd,(struct sockaddr*)&servaddr,servlen); 38 if(ret==-1) 39 perr_exit("connect error!"); 40 41 //get data from the keyboard 42 while(1) 43 { 44 fgets(buf,sizeof(buf),stdin);//hello-->fgets-->"hello\n\0" 45 write(connfd,buf,strlen(buf)); 46 n=read(connfd,buf,sizeof(buf)); 47 write(STDOUT_FILENO,buf,n); 48 } 49 close(connfd); 50 return 0; 51 }
这种情况存在的问题是:一个服务器端只能访问一个客户端,这样是很低效的,因此我们需要设计高并发服务器
10.2 多进程高并发服务器
为了实现多进程高并发服务器,即客户端可以有多个,服务器分别对其进行相应
即每增加一个客户端向服务器端请求数据,此服务器端就直接创建一个子进程进行数据的读写,我们使父进程专门用于和客户端创建连接,子进程专门用于数据的读写处理
server.c
1 #include <stdio.h> 2 #include <sys/socket.h> 3 #include <stdlib.h> 4 #include <arpa/inet.h> 5 #include <ctype.h> 6 #include <unistd.h> 7 #include <string.h> 8 9 #define SERV_PORT 5555 10 #define SERV_IP "127.0.0.1" 11 #define CLIENT_MAX 128 12 #define BUFSIZE 128 13 14 void perr_exit(char *s) 15 { 16 perror(s); 17 exit(1); 18 } 19 20 int main() 21 { 22 int listenfd,connfd; 23 struct sockaddr_in servaddr,cliaddr; 24 int ret,n,i; 25 pid_t pid; 26 char buf[BUFSIZE],str[BUFSIZE]; 27 28 listenfd=socket(AF_INET,SOCK_STREAM,0); 29 if(listenfd<0) 30 perr_exit("socket error!"); 31 32 servaddr.sin_family=AF_INET; 33 servaddr.sin_port=htons(SERV_PORT); 34 inet_pton(AF_INET,SERV_IP,&servaddr.sin_addr.s_addr); 35 ret=bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)); 36 if(ret<0) 37 perr_exit("bind error!\n"); 38 39 listen(listenfd,CLIENT_MAX); 40 41 while(1) 42 { 43 socklen_t clilen=sizeof(cliaddr); 44 connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&clilen); 45 if(connfd<0) 46 perr_exit("accept error!\n"); 47 else 48 printf("receive connection from IP %s at potr %d", 49 inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,str,sizeof(str)), 50 ntohs(SERV_PORT)); 51 pid=fork(); 52 if(pid<0) 53 perr_exit("fork error!\n"); 54 else if(pid==0) 55 { 56 close(listenfd); 57 break; 58 } 59 else 60 { 61 close(connfd); 62 } 63 } 64 65 if(pid==0) 66 { 67 while(1) 68 { 69 n=read(connfd,buf,sizeof(buf)); 70 if(n<0) 71 perr_exit("read error!\n"); 72 else if(n==0) 73 { 74 close(connfd); 75 break; 76 } 77 else 78 { 79 for(i=0;i<n;++i) 80 buf[i]=toupper(buf[i]); 81 write(connfd,buf,n); 82 } 83 } 84 85 } 86 close(listenfd); 87 close(connfd); 88 return 0; 89 }
这种情况出现的问题是:当开启很多个客户端后,客户端处理完后正常退出,在命令行输入ps aux会发现出现了很多僵尸进程
对于父进程而言。,一般要对其子进程进行等待,以防止子进程变成僵尸进程,从而导致内存泄露问题。产生僵尸进程的其中一个原因是子进程结束后向父进程发出SIGCHLD信号,父进程默认忽略了,因此我们可以使父进程调用signal函数,捕捉子进程的SIGCHLD信号。
1 #include <stdio.h> 2 #include <sys/socket.h> 3 #include <stdlib.h> 4 #include <arpa/inet.h> 5 #include <ctype.h> 6 #include <unistd.h> 7 #include <string.h> 8 #include <sys/wait.h> 9 #include <signal.h> 10 #include <sys/types.h> 11 12 #define SERV_PORT 5555 13 #define SERV_IP "127.0.0.1" 14 #define CLIENT_MAX 128 15 #define BUFSIZE 128 16 17 void perr_exit(char *s) 18 { 19 perror(s); 20 exit(1); 21 } 22 23 void wait_child(int signo) 24 { 25 while(waitpid(0,NULL,WNOHANG)>0); 26 return; 27 } 28 29 int main() 30 { 31 int listenfd,connfd; 32 struct sockaddr_in servaddr,cliaddr; 33 int ret,n,i; 34 pid_t pid; 35 char buf[BUFSIZE],str[BUFSIZE]; 36 37 listenfd=socket(AF_INET,SOCK_STREAM,0); 38 if(listenfd<0) 39 perr_exit("socket error!"); 40 41 servaddr.sin_family=AF_INET; 42 servaddr.sin_port=htons(SERV_PORT); 43 inet_pton(AF_INET,SERV_IP,&servaddr.sin_addr.s_addr); 44 ret=bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)); 45 if(ret<0) 46 perr_exit("bind error!\n"); 47 48 listen(listenfd,CLIENT_MAX); 49 50 while(1) 51 { 52 socklen_t clilen=sizeof(cliaddr); 53 connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&clilen); 54 if(connfd<0) 55 perr_exit("accept error!\n"); 56 else 57 printf("receive connection from IP %s at potr %d", 58 inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,str,sizeof(str)), 59 ntohs(SERV_PORT)); 60 pid=fork(); 61 if(pid<0) 62 perr_exit("fork error!\n"); 63 else if(pid==0) 64 { 65 close(listenfd); 66 break; 67 } 68 else 69 { 70 close(connfd); 71 signal(SIGCHLD,wait_child); 72 } 73 } 74 75 if(pid==0) 76 { 77 while(1) 78 { 79 n=read(connfd,buf,sizeof(buf)); 80 if(n<0) 81 perr_exit("read error!\n"); 82 else if(n==0) 83 { 84 close(connfd); 85 break; 86 } 87 else 88 { 89 for(i=0;i<n;++i) 90 buf[i]=toupper(buf[i]); 91 write(connfd,buf,n); 92 } 93 } 94 95 } 96 close(listenfd); 97 close(connfd); 98 return 0; 99 }
10.多路IO转接服务器
之前讲的多进程和多线程也能实现并发服务器,能够实现一个服务器同时接受多个客户端的访问,但是不经常使用的原因是:
使用这种多进程多线程的方式,所有的监听都由服务器(server.c)来做,监听操作等由用户来做,极大的降低了程序的效率并且消耗CPU
解决方式L:多路I/O转接服务器,多路IO转接服务器也叫做多任务IO服务器,该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件
多路IO转接模型基本思想
基本上后面讲的多路IO转接模型都是上述思想,将监听和建立连接等工作都交给内核去做,内核再与server.c进行通信的时候就是有事件发生的时候。
主要使用方式有三种
select
函数描述结构
内核借用select函数来进行监听,函数原型
参数:
参1: 所监听的所有文件描述符中,最大的文件描述符+1,注意其应该是一个int类型的值
但是我们知道文件描述符是可以重复利用的,假如说如上表一样,fd2对应的是500了,那么此时select第一个参数中的nfds应该是501,注意这个对应关系
参2/3/4: fd_set:文件描述符的集合,都是传入传出参数
所监听的文件描述符“可读”事件
所监听的文件描述符“可写”事件
所监听的文件描述符“异常”事件
参4:
此处用到了timeval结构体,结构体形式如下
描述了秒和微秒
返回值:
成功:所监听的所有监听集合中满足条件的总数,具体哪个总数是没有告诉的
失败:返回-1
刚刚select函数的参数和返回值都已经清楚了,但是在其中还有两个地方是比较模糊的
首先是返回函数的集合应该怎么表示呢?
还有就是返回的是成功的总数,但是具体是哪些怎么确定呢?
下面引入以下四个函数
void FD_ZERO(fd_set *set) 将set清空
void FD_CLR(int fd, fd_set *set) 将fd从set中清除出去
void FD_ISSET(int fd, fd_set *set) 判断fd是否在集合中
void FD_SET(int fd, fd_set *set) 将fd设置到set集合中去
借助select机制利用多路IO转接技术来实现并发服务器
如何利用select函数判断监听是否满足条件
由上我们可以看到,假设对于读事件来说,传入参数为fd,fd1,fd2,fd3,因此传入是readfds中上述几个都为1,其余为0
传出参数为fd和fd3,因此这几个参数为1,其余为0
select函数缺点
1. 文件描述符上限--1024
同同时监听的文件描述符--1024个
2. 只能用for循环来判断监听是否满足,当监听文件描述符在总的文件描述符中较少时效率较低
为了解决这个问题,自定义数据结构:数组
3. 监听集合和满足监听条件的集合是一个集合
因此需要将原有集合保存。
11. select实现
假设上图中,细线表示想要建立连接,粗线表示已经建立好了连接
每一个client端都应该首先是建立连接,建立连接的过程实质上是检查是否可读的过程。,当检查到可读时,调用accept()函数建立连接,此时accept()不会阻塞等待事件发生,因为这里是有事件发生了才调用的accept(0函数,注意和之前的区别
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <sys/types.h> 4 #include <sys/socket.h> 5 #include <ctype.h> 6 #include <unistd.h> 7 #include <strings.h> 8 #include <sys/wait.h> 9 10 #define SERV_PORT 5000 11 #define SERV_IP "192.168.0.222" 12 13 int main() 14 { 15 int i, j, n, maxi; 16 int nready, client[FD_SETSIZE]; 17 int maxfd, listenfd, connfd, sockfd; 18 char buf[BUFSIZ], str[INET_ADDRSTRLEN]; 19 struct sockaddr_in clie_addr, serv_addr; 20 socklen_t clie_addr_len; 21 fd_set rset, allset;//每次select监听的结果会将原来的set进行修改,allset保存原来的set 22 listenfd = socket(AF_INET, SOCK_STREAM, 0); 23 bzero(&serv_addr, sizeof(serv_addr)); 24 serv_addr.sin_family = AF_INET; 25 serv_addr.sin_port = htons(SERV_PORT); 26 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); 27 bind(listenfd, (struct sockaddr*) & serv_addr, sizeof(serv_addr)); 28 listen(listenfd, 128); 29 /*****************************************************/ 30 maxfd = listenfd;//起初listenfd即为最大文件描述符 31 maxi = -1;//将来用作client[]的下标,初始值指向0个元素之前的下标位置 32 for (i = 0;i < FD_SETSIZE, i++) 33 { 34 client[i] = -1;//用-1初始化client[] 35 } 36 FD_ZERO(&allset); 37 FD_SET(listenfd, &allset);//allset,即将监听的集合 38 while (1) 39 { 40 rset = allset;//每次循环都重新设置select监控信号集 41 nready = select(maxfd = 1, & rset, NULL, NULL, NULL);//reset:读事件的集合,其是一个传入传出参数 42 if (nready < 0) 43 perr_exit("select error!"); 44 //这个if语句的作用是:判断是否有新的客户端连接请求 45 if (FD_ISSET(listenfd, &rset)) 46 { 47 clie_addr_len = sizeof(clie_addr); 48 connfd = accept(listenfd, (struct sockaddr*) & clie_addr, clie_addr_len); 49 //此处的accept不会发生阻塞,因为是一定有事件发生了才会调用accept,返回新的文件描述符 50 printf("received from %s at port %d\n", 51 inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, str, sizeof(addr)), 52 ntohs(clie_addr.sin_port)); 53 /* 54 下面这段代码的意义是将要监听的文件描述符放到我们自定义的数组中, 55 以避免将来找某个文件描述符满足的时候要去遍历1024,这也是为什么引入自定义数组client的原因 56 */ 57 for (i = 0;i < FD_SETSIZE, i++) 58 { 59 if (client[i] < 0) 60 { 61 client[i] = connfd;//找到clienrt[]中没有使用的位置,保存accept返回的文件描述符到client里 62 break; 63 } 64 } 65 if (i == FD_SETSIZE)//达到select能监控的文件个数上限 66 { 67 fputs("too many clients\n",stderr); 68 exit(1); 69 } 70 FD_SET(connfd, &allset);//向监控文件描述符集合allset添加新的文件描述符connfd 71 if (connfd > maxi) 72 maxfd = connfd;//select第一个参数需要 73 if (i > maxi) 74 maxi = i;//保证maxi存的总是client[]最后一个元素下标 75 76 if (--nready == 0) 77 continue; 78 } 79 80 /*检测哪个client有数据就绪*/ 81 for (i = 0;i < maxi;i++) 82 { 83 if (sockfd = client[i] < 0) 84 continue; 85 if (FD_ISSET(sockfd, &rset))//有读事件发生的时候 86 { 87 if ((n = read(sockfd, buf, sizeof(buf))) == 0)//当client关闭连接时,服务器也关闭对应连接 88 { 89 close(sockfd); 90 FD_CLR(sockfd, &allset); //解除select对此文件描述符的监控 91 client[i] = -1; 92 } 93 else if (n>0) 94 { 95 for (j = 0;j < nj++) 96 buf[j] = toupper(buf[j]); 97 sleep(10); 98 write(sockfd, buf, n); 99 } 100 if (--nready == 0) //跳出for,但还是在while中 101 break; 102 } 103 } 104 } 105 close(listenfd); 106 return 0; 107 }
12. poll函数,其是select函数的升级版
其本质都是一样的如下
在内核中使用poll来进行监听
有如下优点:
(1)可以突破1024,修改配置文件来实现
(2)监听、返回集合是分离的,实现方式:每一个监听的对象都对应数组中的一个元素
(3)搜索范围小
函数原型
nfds:监控数组中有多少文件描述符需要被监控
timeout
-1:阻塞
0:立即返回,不阻塞进程
》0.等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值
fds:数组的首地址 数组类型:struct pollfd fds[5000]; fds[0].fd=listenfd; fds[0].events=POLLIN/PULLOUT/POLLERR; //fds[0].revent会被操作系统设置成对应的默认事件 fds[0].revent
fds[1].fd=fd1;
fds[1].events=POLLOUT
fds[2].fd=fd2;
fds[2].events=POLLOUT
fds[3].fd=fd3;
fds[3].events=POLLOUT
poll(fds,5,-1)
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <sys/types.h> 4 #include <sys/socket.h> 5 #include <ctype.h> 6 #include <unistd.h> 7 #include <strings.h> 8 #include <sys/wait.h> 9 10 #define SERV_PORT 5000 11 #define SERV_IP "192.168.0.222" 12 13 int main() 14 { 15 int listenfd, maxfd, connfd, sockfd; 16 int maxi, n; 17 struct sockaddr_in serv_addr, clie_addr; 18 int nready, client[FD_SETSIZE]; 19 char buf[BUFSIZ]; 20 socklen_t clie_addr_len; 21 struct pollfd, client[OPEN_MAX];//指定打开的最大值,在这里可以修改,不像select固定为1024 22 listenfd = socket(AF_INET, SOCK_STREAM, 0); 23 int opt = 1; 24 setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));//端口复用 25 bzero = (&serv_addr, sizeof(serv_addr)); 26 serv_addr.sin_family = AF_INET; 27 serv_addr.sin_port = htons(SERV_PORT); 28 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); 29 bind(listenfd, (struct sockaddr*) & serv_addr, sizeof(serv_addr)); 30 listen(listenfd, 128); 31 client[0].fd = listenfd; 32 client[0].events = POLLIN; 33 for (int i = 0;i < FOPEN_MAX;i++) 34 { 35 client[i].fd = -1; 36 } 37 maxi = 0; 38 for (;;) 39 { 40 nready = poll(client, maxi + 1, -1);//阻塞监听是否有客户端连接请求 41 if (client[i].revents & POLLIN)//listenfd有读事件就绪,整个大括号都是干这个事儿 42 { 43 clie_addr_len = sizeof(clie_addr); 44 /*====监听客户端请求accept不会阻塞*/ 45 connfd = accept(listenfd, (struct sockaddr*) & clie_addr, clie_addr_len); 46 printf("received connect from %s at port %d", 47 inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)), 48 ntohs(clie_addr.sin_port); 49 for (int i = 1;i < FOPEN_MAX;i++) 50 { 51 if (client[i].fd < 0) 52 { 53 client[i].fd = connfd;//找到client中空闲的位置,存放accept返回的connfd 54 break; 55 } 56 } 57 58 if (i == OPEN_MAX) 59 perr_exit("too many clients"); 60 61 client[i].events = POLLIN; 62 if (i > maxi) 63 maxi = i; 64 if (--nready == 0) 65 { 66 continue; 67 } 68 } 69 70 //前面的if没有满足,说明没有listenfd满足,检测client[]看是哪个connfd就绪 71 for (i = 0;i < maxi, i++) 72 { 73 if (sockfd = client[i] < 0) 74 continue; 75 if (client[i].events&POLLIN) 76 { 77 if ((n = read(sockfd, buf, MAXLINE)) < 0) 78 { 79 if (errno == ECONNRESRT) 80 { 81 printf("client[%d] aborted connection\n", i); 82 close(sockfd); 83 client[i].fd = -1; 84 } 85 } 86 else if(n==0) 87 perr_exit("read error!"); 88 else 89 { 90 for (int j = 0;j < n;j++) 91 { 92 buf[j] = toupper[buf[j]]; 93 } 94 _sleep(10); 95 write(sockfd, buf, n); 96 } 97 if (--nready == 0) 98 break; 99 } 100 } 101 } 102 close(listenfd); 103 return 0; 104 return 0; 105 }
多路IO转接服务器
13.epoll函数
API
epoll ET
epoll LT
epoll非阻塞IO
epoll反应堆模型(libevent核心思想实现)
epoll:LINUX下多路复用IO接口select/poll的增强版本,其能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。因为他会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要侦听的文件描述符集合。另一个原因就是获取事件的时候,他无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入ready队列的描述符集合就行了。
基础API
1. 创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关
这个函数调用完之后会有一个平衡二叉树的树根,如图中的epfd
2. 控制某个epoll监控的文件描述符上的事件:注册,修改,删除
#include <sys/epoll.h> int epoll_ctl(int epfd,int opt,int fd, struct epoll_event *event)//这里的*event表示的是结构体的地址 epfd: 为epoll_creat的句柄 op: 表示动作,用3个宏来表示 EPOLL_CTL_ADD (注册新的fd到epfd); EPOLL_CTL_MOD (修改已经注册的fd的监听事件); EPOLL_STL_DEL (从epfd删除一个fd); event:告诉内核需要监听的事件,其包含两个成员,每个成员又是一个结构体 struct epoll_event { _unit32_t events; //epoll events epoll_data_t data; //user data variable }; typedef union epoll_data { void *ptr; int fd; unit32_t u32; unit64_t u64; } epoll_data_t; EPOLLIN: 对应的文件描述符可以读 EPOLLOUT: 对应的文件描述符可以写 EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来 EPOLLERR: 表示对应的文件描述符发生错误
调用这个函数之后可以将节点一个一个挂在二叉树上,或者从二叉树上将节点删除,还没有开始监听描述符
3.epoll_wait()调用这个函数时-监听文件描述符所用到事件
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout) //这里的*events表示的是数组(是一个传出参数),数组中的每个元素都是struct epoll_event
因此epoll函数的API实现过程
整个实现过程
epoll_create创建树根
epoll_ctl删除修改或者增加节点
epoll_wait阻塞监听
调用epoll_wait之后会将节点上的结构体返回到evts数组中,并将其作为传出参数传出去,因此从数组evts[]中取出的每个文件描述符都是有用的,我们所要做的就是判断每一个文件描述符是读还是写
epol实现
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <string.h> 5 #include <arpa/inet.h> 6 #include <sys/epoll.h> 7 #include<errno.h> 8 #include <ctype.h> 9 10 #define MAXLINE 8192 11 #define SERV_PORT 8000 12 #define OPEN_MAX 5000 13 14 int main() 15 { 16 int i, listenfd, connfd, sockfd; 17 int n, num = 0; 18 int nready.efd, res; 19 char buf[MAXLINE], str[INET_ADDRSTRLEN]; 20 socklen_t clilen; 21 struct sockaddr_in cliaddr, servaddr; 22 struct epoll_event tep, ep[INET_MAX]; 23 listenfd = socket(AF_INET, SOCK_STREAM, 0); 24 int opt = 1; 25 setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));//端口复用 26 bzero(&servaddr, sizeof(servaddr)); 27 servaddr.sin_family = AF_INET; 28 servaddr.sin_port = htons(SERV_PORT); 29 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 30 bind(listenfd, (srtuct sockaddr*)cliaddr, sizeof(cliaddr)); 31 listen(listenfd, 128); 32 efd = epoll_create(OPEN_MAX);//创建epoll模型,efd指向红黑树根节点 33 if (efd == -1) 34 perr_exit("epoll_create error!\n"); 35 tep.events = EPOLLIN; 36 tep.data.fd = listenfd;//指向lfd的监听时间为读 37 res = epoll_ctl(efd, EPOLL_STL_ADD, listenfd, &tep);//将lfd对应的结构体设置到树上,efd可找到该树 38 if (res == -1) 39 perr_exit("epoll_ctl error"); 40 for (;;) 41 { 42 /*epoll为server阻塞监听事件,ep为struct epoll_event类型数组,OPEN_MAX为数组容量,-1表示永久阻塞*/ 43 nready = epoll_wait(efd, ep, OPEN_MAX, -1);//ep表示一个数组,刚进来的时候是空的 44 if (nready == -1) 45 perr_exit("epoll_wait error"); 46 //检查是否有新的连接建立 47 for (i = 0;i < nready;i++ 48 { 49 if (!(ep[i].events & EPOLLIN))//如果不是读事件,继续循环 50 continue; 51 if (ep[i].data.fd == listenfd)//如果满足事件的fd不是lfd 52 { 53 clilen = sizeof(cliaddr); 54 connfd = accept(listenfd, (struct sockaddr*) & cliaddr, clilen);//接收新的连接 55 printf("receive connection from %s at %d\n", 56 inet_ntop(AF_INET, &cliaddr.sin_addr, s_addr, str, sizeof(str)), 57 ntohs(cliaddr.sin_port)) 58 printf("cfd %d ---client %d\n", connfd, ++num); 59 tep.events = EPOLLIN;tem.data.fd = connfd; 60 res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep); 61 if (res == -1) 62 perr_exit("epoll_ctl error"); 63 } 64 else//listenfd不满足,也就是可以客户端有数据过来,要进行数据的读取 65 { 66 sockfd = ep[i].data.fd; 67 n = read(sockfd, buf, MAXLINE); 68 if (n == 0)//读到0,说明客户端关闭连接 69 { 70 res = epoll_ctl(efd, EPOLL_ETL_DEL, sockfd, NULL);//由于读到客户端关闭连接,因此需要将其从红黑树中摘除 71 if (res == -1) 72 perr_exit("epoll_ctl error"); 73 close(sockfd); 74 printf("client[%d] closed connection\n", sockfd); 75 } 76 else if (n < 0) 77 { 78 perr_exit("read n<0 error:"); 79 res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL); 80 close(sockfd); 81 } 82 else//实际读到了字节数 83 { 84 for (i = 0;i < n;i++) 85 buf[i] = toupper(buf[i]);//转大写,写回给客户端 86 write(STDOUT_FILENO, buf, n); 87 writen(sockfd, buf, n); 88 } 89 } 90 } 91 } 92 close(listenfd); 93 close(fd); 94 return 0; 95 }
epoll边沿触发水平触发
epoll除了提供select/poll那种IO事件的电平触发外,还提供了边沿触发,这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率
边沿触发:epoll ET
水平触发:epoll LT
假定下面一种情况
假设epoll监听到需要读数据,那么会将需要读的数据全部放在缓冲区,假设放了1000B在缓冲区,但是调用read()时只读了500B,那么接下来server还会继续去读缓冲区的数据吗?
那么两种思考
1. 应该继续读:因为缓冲区中仍然存在数据,因此应该继续将其读出来
2. 不应该继续读:因为epoll的作用就是告诉server进行读,server也实际上也是读了的,至于是否读完epoll不需要管
那么就像是水平触发和边沿触发
可以打一个比方:比如老师在上课时,看见你一次说你一次今晚写作业,就是边沿触发,但是只给你说需要写作业,回去写不写就是你自己的问题,这就是水平触发
水平触发和边沿触发的服务器端和客户端代码
水平触发更有用,但边沿触发不是一无是处
服务器端:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <string.h> 5 #include <arpa/inet.h> 6 #include <sys/epoll.h> 7 #include<errno.h> 8 #include <ctype.h> 9 10 #define MAXLINE 10 11 #define SERV_PORT 8000 12 13 int main() 14 { 15 struct sockaddr_in servaddr, cliaddr; 16 socklen_t clieaddr_len; 17 int listemfd, connfd; 18 char buf[MAXLINE]; 19 char str[INET_ADDRSTRLEN]; 20 int efd; 21 listenfd = socket(AF_INET, SOCK_STREAM, 0); 22 bzero(&servaddr, sizeof(servaddr)); 23 servaddr.sin_family = AF_INET; 24 servaddr.sin_port = htons(SERV_PORT); 25 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 26 bind(listenfd, (srtuct sockaddr*)cliaddr, sizeof(cliaddr)); 27 listen(listenfd, 128); 28 efd = epoll_create(OPEN_MAX);//创建epoll模型,efd指向红黑树根节点 29 event.events = EPOLLIN | EPOLLET;//ET边沿触发 30 event.events = EPOLLIN;//默认LT水平触发 31 printf("accepting connections ...\n"); 32 cliaddr_len = sizeof(cliaddr); 33 connfd = accept(listenfd, (struct sockaddr*) & cliaddr, clilen);//接收新的连接 34 printf("receive connection from %s at %d\n", 35 inet_ntop(AF_INET, &cliaddr.sin_addr, s_addr, str, sizeof(str)), 36 ntohs(cliaddr.sin_port)); 37 event.data.fd = connfd; 38 epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event); 39 while (1) 40 { 41 res = epoll_wait(efd, resevent, 20, -2); 42 printf("res %d\n", res); 43 if (resevent[0].data.fd == connfd) 44 { 45 len = read(connfd, buf, MAXLINE / 2); 46 write(STDOUT_FILENO, buf, len); 47 } 48 } 49 return 0; 50 }
客户端
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <string.h> 5 #include <arpa/inet.h> 6 #include <sys/epoll.h> 7 #include<errno.h> 8 #include <ctype.h> 9 10 #define MAXLINE 10 11 #define SERV_PORT 8000 12 13 int main() 14 { 15 struct sockaddr_in servaddr; 16 char buf[MAXLINE]; 17 int sockfd, i; 18 char ch = 'a'; 19 sockfd = socket(AF_INET, SOCK_STREAM, 0); 20 bzero(&servaddr, sizeof(servaddr)); 21 servaddr.sin_family = AF_INET; 22 servaddr.sin_port = htons(SERV_PORT); 23 inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); 24 connect(sockfd, (struct sockaddr*) & servaddr, sizeof(servaddr)); 25 while (1) 26 { 27 for (i = 0;i < MAXLINE / 2;i++) 28 { 29 buf[i] = ch; 30 } 31 buf[i - 1] = '\n'; 32 ch++; 33 for (;i < MAXLINE;i++) 34 buf[i] = ch; 35 buf[i - 1] = '\n'; 36 ch++; 37 write(sockfd, buf, sizeof(buf)); 38 _sleep(5); 39 40 } 41 close(sockfd); 42 return 0; 43 }
死锁状态
假定下面一种情况
如图
如图所示,假如server调用readn()函数,其需要读取500个字节,但是实质上客户端只写入了200个字节,此时程序会发生阻塞,但是不是阻塞在epoll处而是阻塞在readn()处。此时如果采用边沿触发方式,再次写入时readn处于阻塞状态无法读取,因此出现问题,这种情况称为死锁
为了解决这个问题,将阻塞干掉,设置成非阻塞
1. fcntl
2. poen--不适用,因为socket不是open打开的而是socket直接创建的套接字
非阻塞IO,边沿式触发
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <string.h> 5 #include <arpa/inet.h> 6 #include <sys/epoll.h> 7 #include<errno.h> 8 #include <ctype.h> 9 10 #define MAXLINE 10 11 #define SERV_PORT 8000 12 13 int main() 14 { 15 struct sockaddr_in servaddr, cliaddr; 16 socklen_t clieaddr_len; 17 int listemfd, connfd; 18 char buf[MAXLINE]; 19 char str[INET_ADDRSTRLEN]; 20 int efd; 21 listenfd = socket(AF_INET, SOCK_STREAM, 0); 22 bzero(&servaddr, sizeof(servaddr)); 23 servaddr.sin_family = AF_INET; 24 servaddr.sin_port = htons(SERV_PORT); 25 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 26 bind(listenfd, (srtuct sockaddr*)cliaddr, sizeof(cliaddr)); 27 listen(listenfd, 128); 28 efd = epoll_create(OPEN_MAX);//创建epoll模型,efd指向红黑树根节点 29 event.events = EPOLLIN | EPOLLET;//ET边沿触发===设置成边沿触发 30 //event.events = EPOLLIN;//默认LT水平触发 31 printf("accepting connections ...\n"); 32 cliaddr_len = sizeof(cliaddr); 33 connfd = accept(listenfd, (struct sockaddr*) & cliaddr, clilen);//接收新的连接 34 printf("receive connection from %s at %d\n", 35 inet_ntop(AF_INET, &cliaddr.sin_addr, s_addr, str, sizeof(str)), 36 ntohs(cliaddr.sin_port)); 37 38 /*==x修改connfd为非阻塞读*/ 39 flag = fcntl(connfd, F_GETFL); 40 flag |= O_NONBLOCK; 41 fcntl(connfd,F_SETFL, flag); 42 event.data.fd = connfd; 43 epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event);//将connfd加入监听红黑树 44 while (1) 45 { 46 res = epoll_wait(efd, resevent, 20, -2); 47 printf("res %d\n", res); 48 if (resevent[0].data.fd == connfd) 49 { 50 while((len = read(connfd, buf, MAXLINE / 2)>0)//非阻塞,轮询 51 write(STDOUT_FILENO, buf, len); 52 } 53 } 54 return 0; 55 }
#include <stdio.h>#include <sys/socket.h>#include <stdlib.h>#include <arpa/inet.h>#include <ctype.h>#include <unistd.h>#include <string.h>
#define SERV_PORT 6000#define SERV_IP "127.0.0.5"#define BUFSIZE 1024
void perr_exit(const char *s){perror(s);exit(-1);}
int main(){int connfd,n,ret;struct sockaddr_in servaddr;char buf[BUFSIZE];connfd=socket(AF_INET,SOCK_STREAM,0);if(connfd==-1)perr_exit("create the socket error!");//initialize the pointermemset(&servaddr,0,sizeof(servaddr));servaddr.sin_family=AF_INET;servaddr.sin_port=htons(SERV_PORT);//servaddr.sin_addr.s_addr=htonl(INADDR_ANY);inet_pton(AF_INET,SERV_IP,&servaddr.sin_addr.s_addr);
//connect the serversocklen_t servlen=sizeof(servaddr);ret=connect(connfd,(struct sockaddr*)&servaddr,servlen);if(ret==-1)perr_exit("connect error!");
//get data from the keyboardwhile(1){fgets(buf,sizeof(buf),stdin);//hello-->fgets-->"hello\n\0"write(connfd,buf,strlen(buf));n=read(connfd,buf,sizeof(buf));write(STDOUT_FILENO,buf,n);}close(connfd);return 0;}
14. epoll反应堆模型
epoll反应堆模型(libevent核心思想实现)
=libevent ---跨平台 精炼---epoll 回调
1. epoll----服务器----监听----fd----可读----epoll返回----read----小写转大写----write----epoll继续监听
2. epoll反应堆模型
epoll----服务器----监听----cfd----可读----epoll返回----read----cfd从树上摘下----重新设置监听cfd,操作----小写转大写----等待epoll_wait 返回----回写客户端----cfd从树上摘下----设置监听cfd读时间,操作----epoll继续监听