【Linux】网络编程

目录

一. C/S与B/S

1.C/S架构(客户端与服务器)

2.B/S架构(浏览器与服务器)

二. 套接字

作用:传输层的文件描述符

三. TCP的C/S实现(循环服务器模型)

1.TCP服务器实现过程

(1)创建套接字 —— socket()

(2)给套接字绑定ip地址和端口号 —— bind()

 *出现无法绑定的问题

(3)为监听做准备 —— listen()

(4)被动监听客户的连接并响应 —— accept()

*usleep()

(5)发送数据 —— send()

(6)接收数据 —— recv()

*注意事项

(7)关闭连接 —— shutdown()  close()

(8) 判断客户端退出

2.TCP客户端的实现过程

(1)创建套接字 —— socket()

(2)进行连接 —— connect()

(3)收发数据 —— send()  recv()

(4)关闭连接 —— shutdown()  close()

3.线程池

四. UDP的C/S实现

1.特点

2.UDP通信过程

(1)调用socket创建套接字文件

(2)bind绑定固定的ip和端口

(3)发送数据 —— sendto()

(4)接收数据 —— recvfrom()

*粘包

3.广播

4.组播

五. 并发服务器模型

六. I/O多路转接(复用)模型 —— 解决“大”并发

*测试自己电脑作服务器能连多少客户端的时候,要修改系统能打开的最大文件描述符数量

1.为什么需要I/O多路复用模型?

(1)并发服务器模型的缺点

(2)循环服务器的缺点

(3)传统的多进程和多线程的服务器无法满足大并发操作

(4)I/O多路复用的工作原理

2.select —— 在做大并发的时候很少用

3.poll —— 在做大并发的时候很少用 

4.epoll

5.封装的库


一. C/S与B/S

1.C/S架构(客户端与服务器)

优点:

  • 由于客户端实现与服务器的直接相连,没有中间环节,因此响应速度快
  • 操作界面漂亮、形式多样,可以充分满足客户自身的个性化要求。
  • C/S结构的管理信息系统具有较强的事务处理能力,能实现复杂的业务流程。

缺点:

  • 需要专门的客户端安装程序,分布功能弱,针对点多面广且不具备网络条件的用户群体,不能够实现快速部署安装和配置。 (功能升级还要更新客户端)
  • 兼容性差,对于不同的开发工具,具有较大的局限性。若采用不同工具,需要重新改写程序。(不同的平台不同的客户端)
  • 开发成本较高,需要具有一定专业水准的技术人员才能完成。

2.B/S架构(浏览器与服务器)

优点:

  • 客户端统一,功能升级只要更新服务器端,不需要去维护客户端
  • 具有分布性特点,可以随时随地进行查询、浏览等业务处理
  • 业务扩展简单方便,通过增加网页即可增加服务器功能。 
  • 维护简单方便,只需要改变网页,即可实现所有用户的同步更新。
  • 开发简单,共享性强

缺点:

  • 表现要达到CS程序的程度需要花费不少精力
  • 在速度和安全性上需要花费巨大的设计成本,这是BS架构的最大问题

 

二. 套接字

作用:传输层的文件描述符

把要发送的数据抽象成文件,套接字就可以看成是文件描述符

 

三. TCP的C/S实现(循环服务器模型)

1.TCP服务器实现过程

(1)创建套接字 —— socket()

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

/*
    功能:
        创建一个套接字文件,然后以文件形式来操作通信,不过套接字文件没有文件名

    参数:
        domian :族/范围,指定协议族
            AF_INET:IPV4,32位
            AF_INET6:IPV6,128位
        type:套接字类型,说白了就是进一步指定,你想要使用协议族中的那个子协议来通信。
            SOCK_STREAM:TCP协议
            SOCK_DGRAM:UDP协议
            SOCK_RDM:表示想使用的是“原始网络通信”,使用ip协议来通信。
            SOCK_NONBLOCK:将socket返回的文件描述符指定为非阻塞的。可以和前面的宏进行 | 运算,        
                           比如: SOCK_STREAM | SOCK_NONBLOCK
            SOCK_CLOEXEC:表示一旦进程exec执行新程序后,自动关闭socket返回的“套接字文件描述
                          符”。也是可以和前面的宏进行 | 运算的,不过一般不指定这个标志。
        protocol:一般置为0,只有在做网络协议测试时才用。

    返回值:
        成功:返回套接字文件描述符。
        失败:返回-1,errno被设置
            
*/

 

(2)给套接字绑定ip地址和端口号 —— bind()

给服务器绑定IP地址和端口号,让客户端能找到自己。客户端不用bind,因为不用别人找到它。

 注意,如果是跨网通信时,绑定的一定是所在路由器的公网ip。

#include <sys/types.h>          
#include <sys/socket.h>
 
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);        

/*            
    功能:
        将指定了通信协议(TCP)的套接字文件与IP以及端口绑定起来。

    参数:
        第一个参数:socket函数创建的套接字
        第二个参数:保存ip和端口号的结构体
        第三个参数:第二个参数的长度

    返回值:
        成功返回0,失败返回-1,errno被设置。

*/

 bind函数第二个参数的结构体

struct sockaddr_in 
{
    sa_family_t     sin_family;     //设置AF_***(地址族)
    __be16          sin_port;       //设置端口号
    struct in_addr  sin_addr;       //设置Ip
};
                           
struct in_addr 
{
    __be32 s_addr; //__be32是32位的unsigned int,因为IPV4的ip是32位的无符号整形数
};
                           
在struct sockaddr_in中,存放端口和ip的成员是分开的,所以设置起来很方便。而struct sockaddr将IP和端口一起放在一个字符数组中,比较麻烦
使用struct sockaddr_in设置后,然后将其强制转为struct sockaddr类型,然后传递给bind函数即可。

struct sockaddr 
{
    sa_family_t sa_family; //指定AF_***,表示使用的什么协议族的IP,
                           //前面说过,协议族不同,ip格式就不同
    char sa_data[14];    //存放ip和端口,”192.168.1.10:55555“
};
  • 结构体已定义,直接用就行
  • 使用结构体前线bzero清零结构体

字节序转换函数(端口号设置需要)

电脑一般是小端字节序(由CPU属性决定),但也有大端字节序,网络是大端字节序传输数据,所以要做一定的转换。

如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

#include <arpa/inet.h>
 
//host to net long 32 bit
uint32_t htonl(uint32_t hostlong);
uint32_t ntohl(uint32_t netlong);
 
//net to host short 16bit
uint16_t htons(uint16_t hostshort);
uint16_t ntohs(uint16_t netshort);

ip地址格式转化(IP设置需要) 

将IP地址在32位整数(网络字节序)与字符串之间进行转换。

注意:

  • 将字符串转为十进制时,该十进制是结构体的结构体的成员,即s_addr接收的。
  • 将十进制转为字符串时,该十进制是结构体的成员,即sin_addr(struct in_addr)接收的
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
/*
   用于将点分十进制表示的IPv4地址转化为网络字节序整数表示的地址
*/
in_addr_t inet_addr(const char *str);
 
/*
   用于转换整数表示的网络字节排序地址为点分十进制的字符串。
   该字符串的空间为静态分配的,这意味着第二次调用该函数时,
   上一次调用将会被重写(覆盖)。
*/
char *inet_ntoa(struct in_addr in);
 

 例.

#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#inlcude <sys.socket.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 33333

int main()
{
    int sockfd;
    
    struct sockaddr_in addr;

    //省去sockfd描述符的创建
    //.......

    bzero(&addr, sizeof(struct, sockaddr_in));
    addr.sin_family = AF_INET;
    addr.sin_prot = htons(PORT);
    addr.sin_addr.s_addr = inet_addr("192.168.1.177");

    if(bind(sockfd, (struct sockaddr *)(&addr), sizeof(struct sockaddr_in)) < 0)
    {
        perror("bind error!");
        exit(1);
    }

    return 0;
}

 *出现无法绑定的问题

有时先终止服务器再终止客户端,再重新重启服务器,会提示“bind error”,要等一会才能重新运行程序。

这是因为,之前讲的四次挥手图片中有个“time wait”字样。

TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。MSL在RFC1122中规定为两分钟;

有一个timeout的值,IP地址仍被占用,系统过两分钟才会释放资源

如何解决:

在server代码的socket()和bind()调用之间插入如下代码:

    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    //setsockopt设置socket状态,SO_REUSEADDR允许bind()过程中本地地址可以重复使用

(3)为监听做准备 —— listen()

作用:并不是监听!将套接字文件描述符,从主动变为被动文件描述符,为监听做准备

主动描述符可以主动的向对方发送数据,被动描述符只能被动的等别人主动想你发数据,然后再回答数据,不能主动的发送数据。socket返回的“套接字文件描述符”默认是主动的,如果你想让它变为被动的话,你需要自己调用listen函数来实现。

而服务器在创建socket时,这个套接字可以读写,是主动的。但服务器和客户端建立连接的三次握手,必须是客户端作为主动方先发出的,所以要把服务器送主动变为被动,让客户端连接自己,然后回答

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
 
int listen(int sockfd, int backlog);

/*
    功能:
        将套接字文件描述符,从主动文件描述符变为被动描述符,然后用于被动监听客户的连接

    参数:
        sockfd:socket所返回的套接字文件描述符
        backlog:指定队列的容量。这个队列用于记录正在连接,但是还没有连接完成的客户,
                一般将队列容量指定为2、3就可以了。这个容量并没有什么统一个设定值,
                一般来说只要小于30即可。

*/

(4)被动监听客户的连接并响应 —— accept()

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
 
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
 
/*
    功能:
        会阻塞,若无人连接就一直阻塞。被动监听客户发起三次握手的连接请求,三次握手成功,
        即建立连接成功。

    参数:
        sockefd:已经被listen转为了被动描述符的“套接字文件描述符”,专门用于被动监听客户的连接。
                如果sockfd没有被listen转为被动描述符的话,accept是无法将其用来监听客户连接的。
        addr:用于记录发起连接请求的那个客户的IP和端口(port)
        addrlen:第二参数addr的大小,不过要求给的是地址。

    返回值:
        成功:返回一个通信描述符(连接描述符),专门用于与该连接成功的客户的通信,
        总之后续服务器与该客户间正式通信,使用的就是accept返回的“通信描述符”来实现的。                       
        失败:返回-1,errno被设置
*/

*usleep()

微秒级的sleep()函数,通常用在连着的read,write函数之间,起到缓冲? 

(5)发送数据 —— send()

#include <sys/types.h>
#include <sys/socket.h>
                    
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
                 
/*
    功能:
        向对方发送数据

    参数:
        sockefd:用于通信的通信描述符,是accept函数返回的不是socket函数返回的
        buf:应用缓存,用于存放你要发送的数据
             正规操作的话,应该使用结构体来封装数据。
        len:buf缓存的大小
        flags:一般设置0,表示这个参数用不到,此时send是阻塞发送的。
            MSG_NOSIGNAL:send数据时,如果对方将“连接”关闭掉了,调用send的进程会
                被发送SIGPIPE信号,这个信号的默认处理方式是终止,所以收到这个信号
                的进程会被终止。  如果给flags指定MSG_NOSIGNAL,表示当连接被关闭时
                不会产生该信号。从这里可看出,并不是只有写管道失败时才会产生SGIPIPE
                信号,网络通信时也会产生这个的信号。
            MSG_DONTWAIT:非阻塞发送   
            MSG_OOB:表示发送的是带外数据

*/

(6)接收数据 —— recv()

#include <sys/types.h>
#include <sys/socket.h>
 
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
         
/*        
    功能:
        接收对方发送的数据

    参数:
        sockfd:通信文件描述符
        buf:应用缓存,用于存放接收的数据
        len:buf的大小
        flags: 0:默认设置,此时recv是阻塞接收的,0是常设置的值。
                MSG_DONTWAIT:非阻塞接收
                MSG_OOB:接收的是带外数据
*/

*注意事项

有时接发数据时也可以用read,write函数, 各有不同和优缺点

数据的发送和接收,收发数据的数据需要在网路中传输,所以同样要进行端序的转换。

  • 发送数据:将主机端序转为网络端序
  • 接收数据:将网络端序转为主机端序

不过只有short、int、float等存储单元字节>1字节的数据,才有转换的需求。如果是char这种存储单元为一个字节的数据,不需要对端序进行转换。

(7)关闭连接 —— shutdown()  close()

  • shutdown():可解决close的弊端,能选择关哪一种读还是写还是都关。有多个文件描述符指向同一个连接时,关一个相当于都关掉。
  • close()
    • 缺点1:会一次性将读写都关掉了,不能单独关其中一种:
    • 缺点2:如果多个文件描述符指向了同一个连接时(通过dup方式复制出其它描述符/子进程继承了这个描述符,所以子进程的描述符也指向了连接),如果只close关闭了其中某个文件描述符时,只要其它的fd还打开着,那么连接不会被断开,直到所有的描述符都被close后才断开连接。
 #include <sys/socket.h>
 
int shutdown(int sockfd, int how);
  
/*               
    功能:
        可以按照要求关闭连接,而且不管有多少个描述符指向同一个连接,只要调用
        shutdown去操作了其中某个描述符,连接就会被立即断开。

    返回值:
        成功返回0,失败返回-1,ernno被设置
                 
    参数:
        sokcfd:TCP服务器断开连接时,使用的是accept所返回的文件描述符 
        how:如何断开连接
            SHUT_RD:只断开读连接
            SHUT_WR:只断开写连接
            SHUT_RDWR:读、写连接都断开
*/

 

    close(int sockfd);

(8) 判断客户端退出

recv返回值如果等于零,客户端就退出了

2.TCP客户端的实现过程

(1)创建套接字 —— socket()

用socket创建套接字文件,指定使用TCP协议

(2)进行连接 —— connect()

调用connect主动向服务器发起三次握手,进行连接

#include <sys/types.h>         
#include <sys/socket.h>
 
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
                    
/*
    功能:
        向服务器主动发起连接请求

    参数:
        sockfd:socket所返回的套接字文件描述符
        addr:用于设置你所要连接服务器的IP和端口。
        addrlen:参数2所指定的结构体变量的大小
                    
    返回值:
        成功返回0,失败返回-1,ernno被设置          
*/

如果只是纯粹的局域网内部通信的话,ip就是局域网IP,但是如果是跨网通信的话,IP必须是服务器所在路由器的公网IP。 

(3)收发数据 —— send()  recv()

(4)关闭连接 —— shutdown()  close()

3.一些现象小结

服务器端的监听socket关闭后,不影响已连接的socket进行通信,但还未连接上的socket就不能连接了。

服务器端recv,客户端send,当客户端异常关闭后,服务器recv的len不断等于0,这样可以用来判断客户端是否已关闭。但这时当服务器异常关闭后,客户端首先没有任何反应,再发两次信息就异常关闭,且send的返回len不会小于0或等于0,就是send这个函数出错导致程序关闭。互换一下服务器send,客户端recv也是同样的反应。

 

4.线程池

用多线程实现的TCP循环服务器的缺点:进程空间有限,线程又是共享进程空间的,一个进程能产生的线程有限。如果有上千上万上百万的用户连接服务器,此方法不现实,这时就要用线程池了。

四. UDP的C/S实现

1.特点

UDP协议没有建立连接特性,所以UDP协议没有自动记录对方IP和端口的特点,每次发送数据时,必须亲自指定对方的IP和端口,只有这样才能将数据发送给对方。

并不存在客户端与服务器,没有被动方,不需要连接。

2.UDP通信过程

(1)调用socket创建套接字文件

(2)bind绑定固定的ip和端口

绑定自己的ip和端口号

(3)发送数据 —— sendto()

#include <sys/types.h>
#include <sys/socket.h>
 
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

/*
    功能:
        发送数据,后两个参数置为NULL和0时功能等同于send
                    
    返回值:
        成功返回发送的字节数,失败返回-1,errno被设置
               
    参数:
        sockfd:socket返回的套接字描述符,对于UDP来说,套接字描述符直接用于通信。
        buf:存放数据的应用缓存
        len:应用缓存的大小。
        flags:一般写0,表示阻塞发送数据,其它常用的选项与send的flags一样。
        dest_addr:填写目标ip和端口前面就讲过,对于UDP来说,UDP没有连接的过程,
                    所以没有自动记录对方的ip和端口,所以每次发送数据的时候,都需
                    要指定对方的ip和端口。
        addelen:dest_addr的大小
*/

(4)接收数据 —— recvfrom()

#include <sys/types.h>
#include <sys/socket.h>
 
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);

/*       
    功能:
        接收数据,最后两个参数与NULL和NULL时,功能与recv功能相同。
                   
    返回值:
        成功返回接收到的字节数,失败返回-1,ernno被设置
 
    参数:
        sockfd:socket返回的套接字文件描述符
        buf:应用缓存,用于存放接收到的数据
        len:buf的大小
        flags:一般写0,表示阻塞接收数据,其它的常用设置同recv函数
        src_addr:用于保存“数据发送方”的ip和端口,以便“接收方”回答对方.如果是局域网通信,
                ip就是局域网ip,如果是广域网通信,ip就是对方的所在路由器的公网ip。如果
                不需要回答对方数据的话,可以不用保存对方的ip和端口,这时recvfrom的最后
                两个参数写NULL,此时与recv函数的功能完全等价。
        addrlen:src_addr的大小
*/

*粘包

为什么会出现粘包:

  • 发送过快,接收方刚接到一个包,又要用这个缓冲区去接另一个包,会把上一次数据又覆盖掉

防止粘包措施:

  • 每次操作时都memset一下缓冲区,清空
  • 每次操作完,都用usleep(3),缓冲一下再去做下一步

3.广播

一个人发,然后其它所有人都接收,这就是广播。

广播只能在局域网内部有效,广播数据是无法越过路由器的,也就是说路由器就是广播数据的边界。

实现方法:ip地址写成广播地址;例如:192.168.1.255

注意:

  • 在同一台机器上演示广播时,因为广播不能自己给自己广播(即发送接收是同一IP),所以接收方在bind自己IP地址是要这样写
    addr.sin_addr.s_addr = htons(INADDR_ANY);     //给它分配一个未使用的IP
  •  广播的一方不用绑定,sendto中的port要写,IP写广播地址,于是信息就会发给局域网中所有IP的port指定端口号
  • 广播的一方要设置广播模式
    int opt = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt));

4.组播

 

五. 并发服务器模型

进程的缺点是连接数多的时候太占内存了,但进程更健壮,一个进程挂不会影响其他的进程,但线程就会互相影响。

一般十个一下的连接用进程,十个以上用线程。

 

六. I/O多路转接(复用)模型 —— 解决“大”并发

*测试自己电脑作服务器能连多少客户端的时候,要修改系统能打开的最大文件描述符数量

做服务器端,用的是服务器版的操作系统,最后服务器端的程序是挂在服务器上运行的,所以要去修改系统磨人的能打开的最大文件描述符数量

1.为什么需要I/O多路复用模型?

(1)并发服务器模型的缺点

举例:辅导学生,一个学生配一个老师,即使学生没有问题,老师也得带待着,白发工资,对CPU开销非常大

  • 多进程实现
    • 优点
      • 编程相对简单,不用考虑线程间的数据同步等,数据通信比较方便
      • (因为进程空间是独立的)服务器健壮,一个进程异常不会影响到其他进程
    • 缺点
      • 资源消耗大。启动一个进程消耗相对比启动一个线程要消耗大很多(要开辟4个G的虚拟空间),同时在处理很多的连接时候需要启动很多的进程多去处理,这时候对系统来说压力就会比较大。
      • 另外系统的进程数限制也需要考虑。(简单判断:进程的pid号是int型,int大小有限,进程数受限)
  • 多线程实现
    • 优点
      • 相对多进程方式,会节约一些资源,会更加高效一些。
    • 缺点
      • 相对多进程方式,增加了编程的复杂度,因为需要考虑数据同步和锁保护。
      • 另外一个进程中不能启动太多的线程(一个进程中创建的线程共享地址空间,4个G一个给内核,三个给用户,分配栈空间每个线程10M)。在Linux系统下线程在系统内部其实就是进程,线程调度按照进程调度的方式去执行的。
      • ulimit -s:查看系统分配的栈的大小
    • 解决方法:(在服务器编程中如何让一个进程中能跑更多的的线程?)限制给每个线程分配的栈空间,修改系统中栈默认的大小,但带来的问题是运行会变得很慢。

(2)循环服务器的缺点

同一时间只能处理一个客户端的问题

举例:只有一个老师,要辅导多个学生,辅导完一个学生才能辅导下一个学生

(3)传统的多进程和多线程的服务器无法满足大并发操作

  • 进程和线程数受限
    • ulimit -u:系统中最多能跑的进程数(理论值,硬件必须跟上)
    • ulimit -s查看栈的大小,算出最大线程的个数
    • 改进方法
      • 修改进程个数,系统硬件资源必须匹配
      • 线程池解决多线程个数受限问题
        • 线程池的缺陷:只能处理短连接,处理完一个就走了。不支持长连接
  • TCP服务器进程同时打开文件数的限制(通信描述符也是文件描述符的一种)
    • 查看进程中最大打开文件个数:ulimit -n
    • 修改方法,见课件
    • 上阿里云租一个

(4)I/O多路复用的工作原理

举例:有一个能力非常强的老师(CPU运行速度很快,处理问题速度快),这位老师回答问题非常迅速,并且可以应对所有的问题。但学生有问题要先举手,老师看到学生举手就立即去解决,10个学生举手,一个一个解决,但解决时间超快。有点像循环服务器,无多进程多线程

 

2.select —— 在做大并发的时候很少用

是一个系统调用,调用时会从用户空间切到内核空间

真正监听这些文件描述符的是内核,把文件描述符地址发给内核,有变化内核就更改这些描述符的状态

#include<sys/select.h>
#include<sys/time.h>

int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timeval *timeout)

/*
    fd_set: 文件描述符集合,实际上是一个数组

    功能:
        监听指定的文件描述符有没有数据可读/可写/出现异常,没有的话就阻塞

    参数:
        maxfd:文件描述符的范围,比待检的最大文件描述符大1(不是数量,是比值大1)
        readfds:被读监控的文件描述符集,不用的话置空,以下两个也是
        writefds:被写监控的文件描述符集
        exceptfds:被异常监控的文件描述符集
        timeout:定时器,设置阻塞时间,阻塞到了之后立马返回。如果没文件符合要求就阻塞,有文件符合要求就继续进行
                0:不管是否有文件满足要求,都立刻返回,无文件满足返回0,有文件满足返回一个正值
                NULL:阻塞进程直到某个文件满足要求
                正整数:等待的最长时间

    返回值:
        返回满足要求的文件描述符的个数,出错返回一个小于零的数

*/

struct timeval        //最终是秒+微妙数形成的时间
{
    __time_t tv_sec;        //秒
    __suseconds_t tv_usec;  //微秒
}

 描述符集的操作

#include <sys/select.h>

void FD_SET(int fd, fd_set *fdset);    //将fd加入描述符集
void FD_CLR(int fd, fd_set *fdset);    //从文件描述符集清除fd
void FD_ZERO(fd_set *fdset);           //清空描述符集
void FD_ISSET(int fd, fd_set *fdset);  //判断fd是否在集合中,在了,就判断fd是否发生变化(原子操作)

文件描述符的状态

  • 可读
  • 可写
  • 异常

优点

  • 优点:几乎在所有的平台上支持,跨平台支持性好(linux,windows都有select操作)

缺点

  • select实现原理是将监控的文件描述符全部切换给内核做监听,跟内核做交互时把所有文件描述符(也就是文件描述符集合)都拷贝给内核,开销非常大,当有上千万个客户端连接,从用户切换到内核的数据传输开销非常大。访问数量影响到系统性能
    • 总结:由于是采用轮询方式全盘扫描,会随着文件描述符FD数量增多而性能下降。
  • select机制使用数组存放监听的文件描述符,fd_set本质是一个数组(静态分配空间,不可变),有最大值,监听个数受限,延展性(拓展性)很差。
    • 总结:数组管理文件描述符,个数受限
  • (每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大)
  • (select支持的文件描述符数量太小了,默认是1024。可修改宏定义,但是效率仍然慢。)

改进

存放文件描述符

3.poll —— 在做大并发的时候很少用 

基本原理与select一致,也是轮询+遍历

唯一的区别就是poll没有最大文件描述符限制(使用链表的方式存储fd),解决了select个数受限的问题

但没有解决select另一个问题,没有解决随着连接数的增加性能下降的问题,从用户态切换到内核拷贝数据的问题

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

/*
    功能:
        监听集合有没有动静,如果没有动静就阻塞,如果有动静就成功返回,返回值为
        集合中有动静的fd的数量。

    参数:
        fds:传递一个struct pollfd结构体数组
        nfds:数组的元素个数
        timeout:超时时间,与select的timeout作用类似
            -1:不设置阻塞时间
            >=0:设置阻塞时间为timeout秒

    返回值:
        -1:说明函数调失败,errno被设置。
        0:超时时间到,而且没有文件描述符有动静。
        >0:返回有响应的文件描述符的数量。
*/
struct pollfd 
{
    int fd;        //文件描述符
    short events;  //设置我们希望发生的事件,比如读事件,这个需要我们自己设置
    short revents; //实际发生的事件,比如读事件,由poll机制自己设置
};
// events:
//    POLLIN 普通或优先级带数据可读
//    POLLRDNORM  普通数据可读
//    POLLRDBAND  优先级带数据可读
//    POLLPRI高优先级数据可读
//    POLLOUT普通数据可写
//    POLLWRNORM  普通数据可写
//    POLLWRBAND  优先级带数据可写
//    POLLERR发生错误
//   POLLHUP发生挂起
//   POLLNVAL  描述字不是一个打开的文件

//如果events与revents相等,就说明fds[1].fd有动静。
if(fds[1].events == fds[1].revents)
{
    //读写fds[1].fd
}

4.epoll

优点

  • 文件描述符用红黑树存放,数量不受限
  • epoll不需要做内核区到用户区的转换,因为数据存在共享内存中。epoll维护的树在共享内存中,内核区和用户区去操作共享内存,因此不需要区域转换,也不需要拷贝操作。

int epoll_create(int size); 

/*
    功能:
        创建存放文件描述符的红黑树,返回epoll描述符

    参数:
        size: epoll上能关注的最大描述符数。(真正用的时候随便穿一个就行,
        epoll在容量不够的时候会做自动扩展)

    返回值:
        返回值:返回红黑树的描述符epollfd,失败返回-1 errno


    epoll在内部是一个红黑树结构,文件描述符默认0-2被终端占用。每次有一个新的文件描述符,
    就创建一个树节点。epoll默认设置的是树上最多的节点数是2000,但是如果数量超过2000,
    则大小会被自动扩展。
*/

 


int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 

/*
    功能:
        为红黑树添加,修改,删除文件描述符,并告诉它要监听描述符的那个事件(放在最后一个struct内)
    
    参数:
        epfd:epoll_create生成的epoll描述符
        op:
            EPOLL_CTL_ADD:注册
            EPOLL_CTL_MOD:修改
            EPOLL_CTL_DEL:删除
        fd:关联的文件描述符
        event:告诉内核要监听什么事件
*/

//epoll红黑树上挂的就是这个结构体
struct epoll_event
{
    uint32_t events;    //控制监听哪个事件
    epoll_data_t data;    //可存放一些自定义数据,一般就只放fd
};

/*
    参数:
        events:
            EPOLLIN:读
            EPOLLOUT:写
            EPOLLERR:异常
*/

typedef uninon epoll_data
{
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
}


//epoll_event可以存储对应文件描述符的描述,而且描述的内容不限制,
//我们可以自定义一些需要存储的属性在一个我们自己写的一个结构体里面,
//让epoll_data里面的ptr指针指向这个结构体里面的地址。因此epoll里面存储数据很灵活。

int epoll_wait(int epfd,struct epoll_event* events,  int maxevents, int timeout);

/*
    功能:
        相当于select函数,等待监听的事件发生,可以设置阻塞时间

    参数:
        epfd: epoll描述符,epoll_create的返回值
        events:用于回传待处理事件的数组,他是一个传出参数,他里面的值是内核拷贝过来的,
            发生了改变的,装有文件描述符的epoll_event结构体
        maxevents:数组的容量(大小个数,就几个内容)
        timeout:阻塞时间
            -1:永久阻塞
            0:立即返回
            >0:监听红黑树里有多少个文件描述符满足要求
*/

 

5.封装的库

c++下可以使用libevent,封装了poll和epoll等函数

 

七. 循环服务器 vs 并发服务器 vs IO多路转接

参考:循环服务器、并发服务器、IO多路转接

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值