bind()listen(),accept()

一、服务器编程框架

模块     单个服务器程序     服务器机群
I/O处理单元     处理客户连接,读写网络数据     作为接入服务器,实现负载均衡
逻辑单元     业务进程或线程     逻辑服务器
网络存储单元     本地数据库 、文件或缓存     数据库服务器
请求队列     各单元之间的通信方式     各服务器之间的永久TCP连接

        I/O处理单元是服务器管理客户连接的模块。主要完成以下工作:等待并接受新的客户连接,接受客户数据,将服务器响应数据返回给客户端。但是,数据的收发不一定在I/O处理单元中执行,也可能在逻辑单元中执行,具体在何处执行取决于事件处理模式。对于一个服务器机群来说,它实现负载均衡,从所有逻辑服务器中选取负荷最小的一台来为新客户服务。

        一个逻辑单元通常是一个线程或者进程。它分析并处理客户数据,然后将结果传递给I/O处理单元或者直接发送给客户端。对服务器机群而言,一个逻辑单元本身是一台逻辑服务器。服务器通常拥有多个逻辑单元,以实现对多个客户任务的并行处理。

网络存储单元可以使数据库、缓存和文件,甚至是一台独立的服务器。但它不是必须的,比如ssh、telnet等登录服务就不需要这个单元。

       请求队列是各单元之间的通信方式的抽象。I/O处理单元收到客户请求时,需要以某种方式通知一个逻辑单元来处理该请求。同样,多个逻辑单元同时访问同一个存储单元时,也需要采用某种机制来协调处理竞态条件。请求队列通常实现为池的一部分。
二、I/O模型
1.阻塞I/O和非阻塞I/O

        阻塞I/O执行的系统调用可能因为无法立即完成而被操作系统挂起,直到等待的事件发生为止。非阻塞I/O执行的系统调用总是立即返回,而不管事件是否已经发生,如果事件没有立即发生则返回-1,和出错情况一样,此时可根据errno来区分这两种情况。对accept/send和recv而言,事件未发生时errno通常被设置为EAGAIN或者EWOULDBLOCK;对于connect而言,errno则被设置成EINPROGRESS。非阻塞I/O一般和I/O通知机制一起使用,如I/O复用和SIGIO信号。
2.I/O复用

       I/O复用是最常使用的I/O通知机制,指定是应用程序通过I/O复用函数向内核注册一组事件,内核通过I/O复用函数把其中就绪的事件通知给应用程序。Linux上常用的I/O复用函数是select、poll和epoll_wait。I/O复用函数本身是阻塞的,它们能提高程序效率的原因在于它们具有同时监听多个I/O事件的能力。
3.四种I/O模型

       阻塞I/O、I/O复用、信号驱动I/O这三者都是同步I/O模型。
I/O模型     读写操作和阻塞阶段
阻塞I/O     程序阻塞与读写函数
I/O复用     程序阻塞与I/O复用系统调用,但可同时监听多个I/O事件。对I/O本身的读写操作是非阻塞的
SIGIO信号     ·信号触发读写就绪事件,用户程序执行读写操作,程序没有阻塞阶段
异步I/O     内核执行读写操作并触发读写完成事件。程序没有阻塞阶段
三、两种高效的事件处理模式

      服务器程序通常要处理三类事件:I/O事件、信号和定时事件。两种高效的事件处理模式:Reactor和Proactor。一般地,同步I/O模型常用于实现Reactor模式,异步I/O模型则用于实现Proactor模式。
1.Reactor模式

        Reactor是一种这样的模式,它要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元)。除此之外,主线程不作任何其他实质性工作。读写数据,接收新的连接,以及处理客户请求均在工作线程完成。

       使用同步I/O模型(以epoll_wait为例)实现的Reactor模式的工作流程是:

       (1)主线程往epoll内核事件表中注册socket读就绪事件;

       (2)主线程调用epoll_wait等待socket上有数据可读;

       (3)当socket上有数据可读时,epoll_wait通知主线程。主线程则将socket可读事件放入请求队列;

      (4)睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket上的写就绪事件;

        (5)主线程调用epoll_wait等待socket可写;

       (6)当socket可写时,epoll_wait通知主线程,主线程将socket可写事件放入请求队列;

       (7)睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果。
2.Proactor模式

         Proactor模式将所有I/O操作都交给主线程和内核来处理,工作线程仅负责业务逻辑。

        使用异步I/O模型(以aio_read和aio_write为例)实现的Proactor模式工作流程是:

        (1)主线程调用aio_read函数想内核注册socket上读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序;

        (2)主线程继续处理其他逻辑;

        (3)当socket上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用;

        (4)应用程序预先定义好的处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求以后,调用aio_write函数想内核注册socket上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序;

        (5)主线程继续处理其他逻辑;

        (6)当用户缓冲区的数据被写入socket之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕;

        (7)应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭socket.
3.两者的区别

        Reactor模式适用于耗时短的处理场景,同时接受多个服务请求,并且一次同步的处理它们的事件驱动程序。

        Proactor则适用于耗时长的处理场景,异步接受和同时处理多个服务器请求的事件驱动程序。
四、两种高效的并发模式

         并发编程的目的是让程序“同时”执行多个任务。而并发模式是指I/O处理单元和多个逻辑单元之间协调完成任务的方法。
1.半同步/半异步模式

        同步是指程序完全按照代码序列的顺序执行;异步是指程序的执行需要有系统事件来驱动,常见的系统事件包括中断、信号等。

        一种高效的半同步/半异步模式是指,主线程只管理监听socket,连接socket由工作线程来管理。主线程是异步的,工作线程也是异步的,这不是严格意义上的半同步/半异步.

 

2.领导者/追随者模式

        领导者/追随者模式是多个工作线程轮流获得事件源集合,轮流监听、分发并处理时间的一种模式。在任意时间点,程序都仅有一个领导者,它负责监听I/O事件,而其他线程则都是追随者,他们休眠在线程池中等待称为新的领导者。当前的领导者如果检测到I/O事件,首先要冲线程池中推选出新的领导者线程,然后处理I/O事件。此时,新的领导者等待新的I/O事件,而原来的领导者则处理I/O事件,二者实现了并发。
五、提高服务器性能的其他建议

        高性能服务器要注意的几个方面:池、数据复制、上下文切换和锁。
1.池

    池是一组资源的集合,这组资源在服务器启动之初就完全被创建好并初始化,这称为静态资源分配。当拂去其正式运行阶段,即开始处理客户请求的时候,如果需要相关资源,则可以直接从池中获取,无需动态分配。因为直接从池中获取资源比动态分配资源的速度要快的多,因为分配系统资源的系统调用都是很耗时的,涉及到用户态和内核态的来回切换。当服务器处理完一个客户连接以后,可以把相关资源放回池中,无需执行系统调用来释放资源。

        常见的池有内存池、进程池、线程池和连接池。
(1)内存池

        内存池通常用于socket的接收缓冲和发送缓冲。内存池的大小可以根据情况进行分配,一种是预先分配好固定的内存池大小,另一种是根据情况动态扩大接收缓冲区。
(2)进程池/线程池

        进程池和线程池都是用于并发的手段,当我们需要一个工作进程或工作线程来处理新到来的客户请求时,我们可以直接从进程的池或线程池中取得一个执行实体,而无须动态调用fork或pthread_create等函数来创建进程和线程。
(3)连接池

        连接池通常用于服务器或服务器机群的内部永久连接。如每个逻辑单元都可能需要频繁访问本地的某个数据库。简单做法是:逻辑单元每次需要访问数据库的时候,就像数据库程序发起连接,而访问完毕后释放连接。这种做法效率太低。一种解决方案是使用连接池。连接池是服务器预先和数据库程序建立的一组连接的集合。当某个逻辑单元需要访问数据库时,它可以直接从连接池中取得一个连接的实体并使用之。待完成后再返回给连接池。
2.数据复制

        尽量使用“零拷贝”函数,如sendfile()、tee()等,从而避免数据在用户空间和内核空间的来回拷贝,提高效率。
3.上下文切换和锁

        不管是多进程还是多线程,数量都不应该太多,否则可能出现进程间或线程间切换占用大量的CPU时间,从而降低效率。

       并发程序中考虑的另外一个问题是共享资源的加锁保护,加的锁应粒度尽可能的小。

 

补充:socket的基础API中,可能被阻塞的系统调用包括accept、send、recv和connect.

listen是不阻塞的;

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
    unsigned short port = 8000;

    int sockfd;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);// 创建通信端点:套接字
    if(sockfd < 0)
    {
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in my_addr;
    bzero(&my_addr, sizeof(my_addr));
    my_addr.sin_family = AF_INET;
    my_addr.sin_port   = htons(port);
    my_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    int err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
    if( err_log != 0)
    {
        perror("binding");
        close(sockfd);
        exit(-1);
    }

    err_log = listen(sockfd, 10);
    if(err_log != 0)
    {
        perror("listen");
        close(sockfd);
        exit(-1);
    }

    printf("listen client @port=%d...\n",port);

    sleep(10);	// 延时10s

    system("netstat -an | grep 8000");	// 查看连接状态

    return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(int argc, char *argv[])
{
	unsigned short port = 8000;        		// 服务器的端口号
	char *server_ip = "10.221.20.12";    	// 服务器ip地址
 
	int sockfd;
	sockfd = socket(AF_INET, SOCK_STREAM, 0);// 创建通信端点:套接字
	if(sockfd < 0)
	{
		perror("socket");
		exit(-1);
	}
	
	struct sockaddr_in server_addr;
	bzero(&server_addr,sizeof(server_addr)); // 初始化服务器地址
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(port);
	inet_pton(AF_INET, server_ip, &server_addr.sin_addr);
	
	int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));      // 主动连接服务器
	if(err_log != 0)
	{
		perror("connect");
		close(sockfd);
		exit(-1);
	}
	
	system("netstat -an | grep 8000");	// 查看连接状态
	
	while(1);
 
	return 0;
}

#include<sys/socket.h>

int listen(int sockfd, int backlog);


本函数的第二个参数规定了内核应该为相应套接口排队的最大连接个数,一般为以下两个队列的大小之和,即未完成三次握手队列 +  已经完成三次握手队列


为了更好的理解backlog参数,我们必须认识到内核为任何一个给定的监听套接口维护两个队列:

1、未完成连接队列(incomplete connection queue),每个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接口处于SYN_RCVD状态。

2、已完成连接队列(completed connection queue),每个已完成TCP三路握手过程的客户对应其中一项。这些套接口处于ESTABLISHED状态。

      当来自客户的SYN到达时,TCP在未完成连接队列中创建一个新项,然后响应以三路握手的第二个分节:服务器的SYN响应,其中稍带对客户SYN的ACK(即SYN+ACK)。这一项一直保留在未完成连接队列中,直到三路握手的第三个分节(客户对服务器SYN的ACK)到达或者该项超时为止(曾经源自Berkeley的实现为这些未完成连接的项设置的超时值为75秒)。如果三路握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。当进程调用accept时,已完成连接队列中的队头项将返回给进程,或者如果该队列为空,那么进程将被投入睡眠,直到TCP在该队列中放入一项才唤醒它。

 

linux系统设置未连接队列最大数限制


linux系统tcp /ip协议栈有个选项可以设置未连接队列大小限制tcp_max_syn_backlog

可以通过命令:cat /proc/sys/net/ipv4/tcp_max_syn_backlog   查看

Linux 系统中提供somaxconn这个参数,它定义了系统中每一个端口最大的监听队列的长度,这是个全局的参数,默认值为128

可以通过命令: cat /proc/sys/net/core/somaxconn 查看


nginx 服务器设置backlog为512


现有我们可以来讨论应用层组件:为何有的应用服务器进程中,会单独使用1个线程,只调用accept方法来建立连接,例如tomcat;有的应用服务器进程中,却用1个线程做所有的事,包括accept获取新连接。

原因在于:首先,SYN队列和ACCEPT队列都不是无限长度的,它们的长度限制与调用listen监听某个地址端口时传递的backlog参数有关。既然队列长度是一个值,那么,队列会满吗?当然会,如果上图中第1步执行的速度大于第2步执行的速度,SYN队列就会不断增大直到队列满;如果第2步执行的速度远大于第3步执行的速度,ACCEPT队列同样会达到上限。第1、2步不是应用程序可控的,但第3步却是应用程序的行为,假设进程中调用accept获取新连接的代码段长期得不到执行,例如获取不到锁、IO阻塞等。

那么,这两个队列满了后,新的请求到达了又将发生什么?
若SYN队列满,则会直接丢弃请求,即新的SYN网络分组会被丢弃;如果ACCEPT队列满,则不会导致放弃连接,也不会把连接从SYN列队中移出,这会加剧SYN队列的增长。所以,对应用服务器来说,如果ACCEPT队列中有已经建立好的TCP连接,却没有及时的把它取出来,这样,一旦导致两个队列满了后,就会使客户端不能再建立新连接,引发严重问题。
所以,如TOMCAT等服务器会使用独立的线程,只做accept获取连接这一件事,以防止不能及时的去accept获取连接。

那么,为什么如Nginx等一些服务器,在一个线程内做accept的同时,还会做其他IO等操作呢?
这里就带出阻塞和非阻塞的概念。应用程序可以把listen时设置的套接字设为非阻塞模式(默认为阻塞模式),这两种模式会导致accept方法有不同的行为。对阻塞套接字,accept行为如下图:


这幅图中可以看到,阻塞套接字上使用accept,第一个阶段是等待ACCEPT队列不为空的阶段,它耗时不定,由客户端是否向自己发起了TCP请求而定,可能会耗时很长。
对非阻塞套接字,accept会有两种返回,如下图:


非阻塞套接字上的accept,不存在等待ACCEPT队列不为空的阶段,它要么返回成功并拿到建立好的连接,要么返回失败。

所以,企业级的服务器进程中,若某一线程既使用accept获取新连接,又继续在这个连接上读、写字符流,那么,这个连接对应的套接字通常要设为非阻塞。原因如上图,调用accept时不会长期占用所属线程的CPU时间片,使得线程能够及时的做其他工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值