精华内容
下载资源
问答
  • 不管什么服务器,也不管你用什么语言开发服务器。这些都要注意。(如果方法不一样,那只是语言给你封装好了。) 每个服务器在创建 的时候 都会 有 Listen这个或者功能。服务器开始监听链接。其中里面有个int 类型的...

    不管什么服务器,也不管你用什么语言开发服务器。这些都要注意。(如果方法不一样,那只是语言给你封装好了。)

    1. 每个服务器在创建 的时候 都会 有 Listen这个或者功能。服务器开始监听链接。其中里面有个int 类型的参数。我在之前一直以为,这个int类型 表示的 是 可以链接的客户数量,其实不然,这个是每毫秒可以用户访问的并发数量。 比如,在同一毫秒时间宽度内,有三个客户同时链接,而这个参数确实2。那么这一毫秒他就只接收两个客户的请求。

    2. 然后会调用accept这个方法。 通常会有一个参数是你 刚才已经在监听模式的Socket的句柄。 还有两个参数会是链接进来的客户端的 地址。 然他会有一个返回值。或者你所使用的语言已经封装了这个返回值。 这个不重要 。 重要的是,每次有个新用户链接, 他都会重新生成一个新的Socket 的 句柄,用来专门和这个客户端进行收发数据和关闭链接等等操作。 而 第一次原先的那个socket 还会进行他等待其他客户端链接请求的使命。

    3. (2)中我们知道。每次一个客户端的链接,都是实例一个新的Socket,而每一个Socket 都是会占用一个端口的,只是他在实例的时候会自动选择未被占用的端口。这样,如果你的游戏服务应用,数据库服务应用,什么商场服务应用,用户信息服务应用等等……各种服务应用都在一个服务器上面运行。这样就会很消耗服务器的承载量,会造成明显的延时卡顿,如果你使用了各种优化,各种策略之后还是会造成极大的网络延时,那么,就把他们 分别部署到不同的 服务器上面吧

    4. 大型网络游戏,如果客户访问量够大的话,需要引用一个集群化的方法,把所有链接请求分发出去。所有的服务器 由功能可以分为“前置接收任务”,“后置处理任务”两组服务器。“接收任务”负责接收客户端的 任务请求,然后查看 “处理任务“服务器组那个服务器空闲,空闲则直接由这个 服务器 来完成 用户 ”任务“。这样就可以保障一台服务器概念下永远不会超过自己的承载量。

    5. 要注意的是在数据的收发方法 int send(...); int recv(...);中。(C++语言)其中参数 类型 Char*是表示提供数据缓冲的类型。虽然函数原型中注明是Char*类型,但是实际上可以接收任何类型的缓冲指针并做强制类型转化即可
      那为什么还要用Char*这一类型呢,其实在在早期的时候 C++语言中没有 Void* 这个概念。然后后期才加入的。而这个Void*在C++语言中又明确规定: 任何类型的指针,都可以转化为Void*
      那么其实,这个Char* 你完全可以看做是Void*

    6. 在收发数据的时候需要注意。一方调用Send发送的时候,另一方应该调用Recv接收。如果两端同时 Send 或者 Recv 那么稍后一端的调用就会失败。 如果Recv调用指定的缓冲过于小的时候,一般这时候返回值是实际接收的字节数,当这个值等于缓冲长度的时候,要考虑循环调用Recv直到返回值小于缓冲长度,以接收所有的数据。

    展开全文
  • Python客户端/服务器网络编程

    千次阅读 2018-01-08 09:56:02
    服务器响应客户端请求之前,首先创建一个通信端点,它能够使服务器监听请求。一旦一个通信端点已经建立,监听服务器就可以进入无限循环中,等待客户端的连接并响应他们的请求。通信端点好比是允许通信的一些基础...

    在服务器响应客户端请求之前,首先创建一个通信端点它能够使服务器监听请求。一旦一个通信端点已经建立,监听服务器就可以进入无限循环中,等待客户端的连接并响应他们的请求。通信端点好比是允许通信的一些基础设施

    客户端所需要做的只是创建它的单一通信端点,然后建立一个到服务器的连接。然后,客户端就可以发出请求,该请求包括任何必要的数据交换。一旦请求被服务器处理,且客户端收到结果或某种确认信息,此次通信就会被终止。



    套接字:通信端点


    套接字是计算机网络数据结构,它体现了上面所述的“通信端点”的概念。在任何类型的通信开始之前,网络应用程序都必须创建套接字,它就像是电话插孔,没有它将无法进行通信。



    套接字有两种类型:基于文件的和面向网络的。


    第一种套接字是基于文件的,并拥有一个“家族名字”AF_UNIX(又名AF_LOCAL),它代表地址家族:UNIX。AF_LOCAL将代替AF_UNIX,然而考虑到后向兼容性,很多系统都同时使用二者,只是对同一个常数使用不同的别名。Python本身仍然在使用AF_UNIX。


    第二种套接字是基于网络的,它也有自己的家族名字AF_INET,它代表地址家族:因特网。另一个地址家族AF_INET6用于第6版因特网协议(IPv6)寻址。


    此外,还有其他的地址家族,这些要么是专业的、过时的、很少使用的,要么是仍未实现的。在所有的地址家族中,目前AF_INET是使用得最广泛得。

    总的来说,Python只支持AF_UNIX、AF_NETLINK、AF_TIPC和AF_INET家族。网络编程涉及AF_INET。


    套接字地址:主机-端口号

    如果套接字像一个电话插孔,那么主机名和端口号就像区号和电话号码的组合。在拥有硬件和通信的能力后,你还要知道电话打给谁。一个网络地址由主机名和端口号对组成。有效的端口号范围为0~65535(小于1024的端口号预留给了系统),众所周知的端口号列表可以在这个网站中查看:http://www.iana.org/assignments/port-numbers



    套接字连接有两种风格:面向连接的和无连接的。

    不管采用哪种地址家族,都有两种不同风格的套接字连接。


    1、面向连接的套接字


    面向连接意味着在进行通信之前必须先建立一个连接,例如使用电话系统给一个朋友打电话。这种类型的通信也称为虚拟电路或流套接字。

    特点:每条消息可以拆分成多个片段,并且每一条消息片段都确保能够到达目的地,然后将它们按顺序组合在一起,最后将完整消息传递给正在等待的应用程序。

    协议:实现这种连接类型的主要协议是传输控制协议(TCP)。为了创建TCP套接字,必须使用SOCK_STREAM作为套接字类型。TCP套接字的名字SOCK_STREAM基于流套接字的其中一种表示。因为这些套接字(AF_INET)使用因特网协议(IP)来搜寻网络中的主机,所以整个系统通常结合两种协议(TCP和IP)。


    2、无连接的套接字


    此类套接字是数据报类型的套接字,是一种无连接的套接字,这意味着在通信开始之前并不需要建立连接,例如邮政服务,信件和包裹或许并不能以发送顺序到达,事实上他们可能不会到达,为了将其添加到并发通信中,在网络中甚至可能存在重复的消息。

    特点:在数据传输过程中并无法保证它的顺序性、可靠性或重复性。然而,数据报确实保存了记录边界,这意味着消息是以整体发送的,而并非首先分成多个片段。

    协议:主要协议是用户数据报协议(UDP)。为了创建UDP套接字,必须使用SOCK_DGRAM作为套接字类型。因为这些套接字也使用因特网协议(IP)来搜寻网络中的主机,所以整个系统通常结合两种协议(UDP和IP)。




    Python中的网络编程


    在Python中使用的主要模块是socket模块,在这个模块中可以找到socket()函数,该函数用于创建套接字对象,套接字有自己的方法集,这些方法可以实现基于套接字的网络通信。



    socket()函数


    socket.socket()函数用于创建套接字,一般语法如下:

    socket(socket_family, socket_type, protocol=0)

    socket_family是AF_UNIX或AF_INET(如前所述),socket_type是SOCK_STREAM或SOCK_DGRAM(如前所述),protocol通常省略,默认为0。


    所以,创建TCP/IP套接字

    tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    创建UDP/IP套接字

    udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)



    套接字对象(内置)方法

    (以下方法只为简易的一部分,更多应参考API)


    服务器套接字方法

     s.bind()  将地址(主机名、端口号对)绑定到套接字上   
     s.listen()  设置并启动TCP监听器
     s.accept()  被动接收TCP客户端连接,一直等待直到连接到达(阻塞)


    客户端套接字方法

     s.connect()  主动发起TCP服务器连接     
     s.connect_ex()  connect()的扩展版本,此时会以错误码的形式返回问题,而不是抛出一个异常   


    普通的套接字方法

     s.recv()  接收TCP消息  
     s.send()  发送TCP消息
     s.sendall()  完整地发送TCP消息
     s.recvfrom()  接受UDP消息
     s.sendto()  发送UDP消息
     s.shutdown()  关闭连接
     s.close()  关闭套接字


    面向阻塞的套接字方法

     s.setblocking()  设置套接字的阻塞或非阻塞模式
     s.settimeout()  设置阻塞套接字操作的超时时间
     s.gettimeout()  获取阻塞套接字操作的超时时间



    创建TCP服务器


    设计服务器的一种方式:


    from socket import *

    ss = socket( )                           #创建服务器套接字

    ss.bind( )                              #套接字与地址绑定 

    ss.listen( )                             #监听连接

    inf_loop:                               #服务器无限循环 

        cs = ss.accept( )                     #接受客户端连接 → (开启服务器)

        comm_loop:                          #通信循环 → (通信开始)

            cs.recv( ) / cs.send( )             #对话(接受/发送) 

        cs.close( )                         #关闭客户端套接字

    ss.close( )                             #关闭服务器套接字(可选)


    (这仅仅是设计服务器的一种方式,一旦熟悉了服务器设计,那么你将能够按照自己的要求修改上面的伪代码来操作服务器)


      调用accept()函数之后,就开启了一个简单的(单线程)服务器,它会等待客户端的连接,默认情况下,accept()是阻塞的,这意味着执行将被暂停,直到一个连接到达。非阻塞模式的套接字可以参考其他文档与资料。

      一旦服务器接受了一个连接,就会返回(利用accept())一个独立的新的客户端套接字,用来与即将到来的消息进行交换,类似与将客户的电话切换给客服代表。这将能够空出主要的端口(原始服务器套接字),以使其能够接受新的客户端连接。

      一旦创建了临时套接字,通信就可以开始,通过使用这个新的套接字,客户端与服务器就可以开始参与发送和接受的对话中,直到连接终止。当一方关闭连接或者向对方发送一个空字符串时,通常就会关闭连接。在代码中,一个客户端连接关闭之后,服务器就会等待另一个客户端连接。



    创建TCP客户端

     

    from socket import *                    

    cs = socket()                            #创建客户端套接字

    cs.connect()                             #尝试连接服务器

    comm_loop:                              #通信循环  

        cs.send()/cs.recv()                    #对话(发送/接受)

    cs.close()                              #关闭客户端套接字



    执行TCP服务器和客户端


    在任何客户端试图连接之前,首先要启动服务器。服务器可以视为一个被动伙伴,因为必须首先建立自己,然后被动地等待连接,客户端是一个主动的合作伙伴,由它主动发起一个连接。



    创建UDP服务器


    from socket import *                        

    ss = socket()                              #创建服务器套接字

    ss.bind()                                 #绑定服务器套接字

    inf_loop:                                 #服务器无限循环

        cs = ss.recvfrom() / ss.sendto()             #对话(接受/发送)

    ss.close()                                #关闭服务器套接字


    UDP服务器不需要TCP服务器那么多的设置,没有调用“监听传入的连接”,因为它不是面向连接的。另一个显著差异是,因为数据报套接字是无连接的,所以就没有为了成功通信而使一个客户端连接到一个独立的套接字“转换”操作,这种服务器仅仅接受消息并有可能回复数据。



    创建UDP客户端


    from socket import *

    cs = socket()                               #创建客户端套接字

    comm_loop:                                 #通信循环

        cs.sendto() / cs.recvfrom()                  #对话(发送/接受)

    cs.close()                                 #关闭客户端套接字


    UDP客户端循环工作方式几乎和TCP客户端完全一样。唯一的区别是,事先不需要建立与UDP服务器的连接,只是简单地发送一条消息并等待服务器的回复。




    SocketServer模块


    SocketServer模块是标准库中的一个高级模块(Python3.x中重命名为socketserver),它的目标是简化很多样板代码,他们是创建网络客户端和服务器所必须的代码。这个模块中有各种各样的类。


    以下为一部分类:

     TCPServer / UDPServer  基础的网络同步TCP/UDP服务器
     UnixStreamServer / UnixDatagramServer  基于文件的基础同步TCP/UDP服务器
     ForkingTCPServer / ForkingUDPServer  ForkingMixIn和TCPServer / UDPServer的组合
     ThreadingTCPServer / ThreadingUDPServer  ThreadingMixIn和TCPServer / UDPServer的组合
     StreamRequestHandler / DatagramRequestHandler  实现TCP/UDP服务器的服务处理器



      使用SocketServer模块除了可以隐藏实现细节外,另一个不同之处是,我们现在使用类来编写应用程序。因为以面向对象的方式处理事务有助于组织数据,以及逻辑性地将功能放在正确的地方。你还会注意到,应用程序现在是事件驱动的,事件包括消息的发送和接受,这意味着只有在系统中的事件发生时,他们才会工作。
      在原始服务器循环中,我们阻塞等待请求,当接受到请求时就对其提供服务,然后继续等待。而此处的服务器循环中,并非在服务器中创建代码,而是定义一个处理程序,这样当服务器接收到一个传入的请求时,服务器就可以调用你的函数。



    创建SocketServer TCP服务器


    示例(Python2.x):

    from SocketServer import (TCPServer as TCP, StreamRequestHandler as SRH)
    
    HOST = ''
    PORT = 21567
    ADDR = (HOST, PORT)
    
    class MyRequestHandler(SRH):
        def handle(self):
            self.wfile.write('%s' % self.rfile.readline())
    
    tcpServ = TCP(ADDR, MyRequestHandler)
    tcpServ.serve_forever()


    相关解释:

      这里的请求处理程序MyRequestHandler,作为SocketServer中StreamRequestHandler的一个子类,并重写了它的handle()方法,该方法在基类Request中默认情况下没有任何行为。当接受到一个来自客户端的消息时,它就会调用handle()方法。
      StreamRequestHandler类将输入和输出套接字看作类似文件的对象,因此我们将使用readline()来获取客户端消息,并利用write()将字符串发送会客户端,因此在客户端和服务器代码中,需要额外的回车和换行符,但实际上,在服务器的代码中不会看到它,因为重用了那些来自客户端的符号。



    创建SocketServer TCP客户端


    SocketServer TCP客户端和原来的客户端的代码类似,主要不同点在于:


      SocketServer请求处理程序的默认行为是接受连接、获取请求,然后关闭连接。由于这个原因,应用程序不能在整个执行过程中都保持连接,因此每次向服务器发送消息时,都需要创建一个新的套接字。

      另外StreamRequestHandler类对待套接字通信就像文件一样,所以必须发送行终止符(回车和换行符),例如tcpCliSock.send('%s\r\n' % data),而服务器只是保留并重用这里发送的终止符。





    展开全文
  • 5种服务器网络编程模型

    千次阅读 2017-10-08 20:40:06
    多进程模型和多线程(线程池)模型每个进程/线程只能处理一路IO,在服务器并发数较高的情况下,过多的进程/线程会使得服务器性能下降。而通过多路IO复用,能使得一个进程同时处理多路IO,提升服务器吞吐量。 在...

    1.同步阻塞迭代模型

    同步阻塞迭代模型是最简单的一种IO模型。

    其核心代码如下:

    [cpp] view plain copy
    1. bind(srvfd);  
    2. listen(srvfd);  
    3. for(;;){  
    4.     clifd = accept(srvfd,...); //开始接受客户端来的连接  
    5.     read(clifd,buf,...);       //从客户端读取数据  
    6.     dosomthingonbuf(buf);    
    7.     write(clifd,buf)          //发送数据到客户端  
    8. }  

    上面的程序存在如下一些弊端:accept,read,write都可能阻塞

    1)如果没有客户端的连接请求,进程会阻塞在accept系统调用处,程序不能执行其他任何操作。(系统调用使得程序从用户态陷入内核态)

    2)在与客户端建立好一条链路后,通过read系统调用从客户端接受数据,而客户端合适发送数据过来是不可控的。如果客户端迟迟不发生数据过来,则程序同样会阻塞在read调用,此时,如果另外的客户端来尝试连接时,都会失败。

    3)同样的道理,write系统调用也会使得程序出现阻塞(例如:客户端接受数据异常缓慢,导致写缓冲区满,数据迟迟发送不出)。

    2.多进程并发模型

    同步阻塞迭代模型有诸多缺点。多进程并发模型在同步阻塞迭代模型的基础上进行了一些改进,以避免是程序阻塞在read系统调用上。

    多进程模型核心代码如下:

    [cpp] view plain copy
    1. bind(srvfd);  
    2. listen(srvfd);  
    3. for(;;){  
    4.     clifd=accept(srvfd,...);//开始接受客户端来的连接  
    5.     ret=fork();  
    6.     switch(ret)  
    7.     {  
    8.       case-1:  
    9.         do_err_handler();  
    10.         break;  
    11.       case0  :  // 子进程  
    12.         client_handler(clifd);  
    13.         break;  
    14.       default:  // 父进程  
    15.         close(clifd);  
    16.         continue;  
    17.     }  
    18. }  
    19. //======================================================  
    20. voidclient_handler(clifd){  
    21.     read(clifd,buf,...);      //从客户端读取数据  
    22.     dosomthingonbuf(buf);    
    23.     write(clifd,buf)          //发送数据到客户端  
    24. }  

    上述程序在accept系统调用时,如果没有客户端来建立连接,择会阻塞在accept处。一旦某个客户端连接建立起来,则立即开启一个新的进程来处理与这个客户的数据交互。避免程序阻塞在read调用,而影响其他客户端的连接。

    3.多线程并发模型

    在多进程并发模型中,每一个客户端连接开启fork一个进程,虽然linux中引入了写实拷贝机制,大大降低了fork一个子进程的消耗,但若客户端连接较大,则系统依然将不堪负重。通过多线程(或线程池)并发模型,可以在一定程度上改善这一问题。

    在服务端的线程模型实现方式一般有三种:

    (1)按需生成(来一个连接生成一个线程)

    (2)线程池(预先生成很多线程)

    (3)Leader follower(LF)

    为简单起见,以第一种为例,其核心代码如下:

    [cpp] view plain copy
    1. void *thread_callback( void *args ) //线程回调函数  
    2. {  
    3.         int clifd = *(int *)args ;  
    4.         client_handler(clifd);  
    5. }  
    6. //===============================================================  
    7. void client_handler(clifd){  
    8.     read(clifd,buf,...);       //从客户端读取数据  
    9.     dosomthingonbuf(buf);    
    10.     write(clifd,buf)          //发送数据到客户端  
    11. }  
    12. //===============================================================  
    13. bind(srvfd);  
    14. listen(srvfd);  
    15. for(;;){  
    16.     clifd = accept();  
    17.     pthread_create(...,thread_callback,&clifd);  
    18. }  

    服务端分为主线程和工作线程,主线程负责accept()连接,而工作线程负责处理业务逻辑和流的读取等。因此,即使在工作线程阻塞的情况下,也只是阻塞在线程范围内,对继续接受新的客户端连接不会有影响。

    第二种实现方式,通过线程池的引入可以避免频繁的创建、销毁线程,能在很大程序上提升性能。但不管如何实现,多线程模型先天具有如下缺点:

    1)稳定性相对较差。一个线程的崩溃会导致整个程序崩溃。

    2)临界资源的访问控制,在加大程序复杂性的同时,锁机制的引入会是严重降低程序的性能。性能上可能会出现“辛辛苦苦好几年,一夜回到解放前”的情况。

    4.IO多路复用模型之select/poll

    多进程模型和多线程(线程池)模型每个进程/线程只能处理一路IO,在服务器并发数较高的情况下,过多的进程/线程会使得服务器性能下降。而通过多路IO复用,能使得一个进程同时处理多路IO,提升服务器吞吐量。

    在Linux支持epoll模型之前,都使用select/poll模型来实现IO多路复用。

    以select为例,其核心代码如下:

    [cpp] view plain copy
    1. bind(listenfd);  
    2. listen(listenfd);  
    3. FD_ZERO(&allset);  
    4. FD_SET(listenfd,&allset);  
    5. for(;;){  
    6.     select(...);  
    7.     if(FD_ISSET(listenfd,&rset)){    /*有新的客户端连接到来*/  
    8.         clifd=accept();  
    9.         cliarray[]=clifd;      /*保存新的连接套接字*/  
    10.         FD_SET(clifd,&allset);  /*将新的描述符加入监听数组中*/  
    11.     }  
    12.     for(;;){    /*这个for循环用来检查所有已经连接的客户端是否由数据可读写*/  
    13.         fd=cliarray[i];  
    14.         if(FD_ISSET(fd,&rset))  
    15.             dosomething();  
    16.     }  
    17. }  

    示例代码:

    /*************************************************************************
      > Description:使用select函数实现I/O复用服务器端
     ************************************************************************/
    
    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    #include<unistd.h>
    #include<arpa/inet.h>
    #include<sys/socket.h>
    #include<sys/time.h>
    #include<sys/select.h>
    
    void error_handling(char *message);
    
    #define BUFF_SIZE 32
    
    int main(int argc, char *argv[])
    {
    	int server_sock;
    	int client_sock;
    
    	struct sockaddr_in server_addr;
    	struct sockaddr_in client_addr;
    	socklen_t client_addr_size;
    
    	char buff[BUFF_SIZE];
    	fd_set reads, reads_init;
    	struct timeval timeout, timeout_init;
    
    	int str_len, i, fd_max, fd_num;
    
    	if(argc!=2){ //命令行中启动服务程序仅限一个参数:端口号
    		printf("Usage : %s <port>\n", argv[0]);
    		exit(1);
    	}
    	
    	//调用socket函数创建套接字
    	server_sock = socket(PF_INET, SOCK_STREAM, 0);
    	if(-1 == server_sock){
    		error_handling("socket() error.");
    	}
    
    	memset(&server_addr, 0, sizeof(server_addr));
    	server_addr.sin_family = AF_INET;
    	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    	server_addr.sin_port = htons(atoi(argv[1]));
    	
    	//调用bind函数分配IP地址和端口号
    	if( -1 == bind( server_sock, (struct sockaddr*)&server_addr, 
    				sizeof(server_addr)) ){
    		error_handling("bind() error");
    	}
    
    	//监听端口的连接请求,连接请求等待队列size为5
    	if( -1 == listen(server_sock, 5) ){
    		error_handling("listen() error");
    	}
    
    	//register fd_set var
    	FD_ZERO(&reads_init);
    	FD_SET(server_sock, &reads_init);//monitor socket: server_sock
    	FD_SET(0, &reads_init);// stdin also works
    	fd_max = server_sock;
    	//
    	timeout_init.tv_sec = 5;
    	timeout_init.tv_usec= 0;
    
    	while(1){
    		//调用select之后,除发生变化的文件描述符对应的bit,其他所有位置0,所以需用保存初值,通过复制使用
    		reads = reads_init;
    		//调用select之后,timeval成员值被置为超时前剩余的时间,因此使用时也需要每次用初值重新初始化
    		timeout = timeout_init;
    		fd_num = select(fd_max+1, &reads, NULL, NULL, &timeout);
    		if(fd_num < 0){
    			fputs("Error select()!", stderr);
    			break;
    		}else if(fd_num == 0){
    			puts("Time-out!");
    			continue;
    		}
    		for(i=0; i<=fd_max; i++){
    			if(FD_ISSET(i, &reads)){
    				if(i == server_sock){//connection request!
    					//接受连接请求
    					client_addr_size = sizeof(client_addr);
    					client_sock = accept( server_sock, (struct sockaddr*)&client_addr, &client_addr_size );
    					//accept函数自动创建数据I/0 socket
    					if(-1 == client_sock){
    						error_handling("accept() error");
    						//健壮性不佳,程序崩溃退出
    					} else{
    						//注册与客户端连接的套接字文件描述符
    						FD_SET(client_sock, &reads_init);
    						if(fd_max < client_sock) fd_max = client_sock;
    						printf("Connected client : %d\n", client_sock);
    					}
    				}else{//read message!
    					str_len = read(i, buff, BUFF_SIZE);
    					if(str_len){//echo to client
    						buff[str_len] = 0;
    						printf("Message from client %d: %s", i, buff);
    						write(i, buff, str_len);
    					}else{ //close connection
    						FD_CLR(i, &reads_init);
    						close(i);
    						printf("Disconnected client %d!\n", i);
    					}
    				}//end of i==|!=server_sock
    			}//end of if(FD_ISSET)
    		}//end of while
    
    	}//end of for
    
    	//断开连接,关闭套接字
    	close(server_sock);
    
    	return 0;
    }
    
    void error_handling(char *message)
    {
    	fputs(message, stderr);
    	fputc('\n', stderr);
    	exit(EXIT_FAILURE);
    }
    


    select IO多路复用同样存在一些缺点,罗列如下:

    1. 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE    1024)
    2. 内核 / 用户空间内存拷贝问题select需要复制大量的句柄数据结构,产生巨大的开销;
    3. select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件
    4. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

    相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。

    拿select模型为例,假设我们的服务器需要支持100万的并发连接,则在__FD_SETSIZE 为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。

    5.IO多路复用模型之epoll

    epoll IO多路复用:一个看起来很美好的解决方案。 由于文章:高并发网络编程之epoll详解中对epoll相关实现已经有详细解决,这里就直接摘录过来。

    由于epoll的实现机制与select/poll机制完全不同,上面所说的 select的缺点在epoll上不复存在。

    设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?

    在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。

    epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+树,实际为红黑树+双端链表)。把原先的select/poll调用分成了3个部分:

    1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)

    2)调用epoll_ctl向epoll对象中添加这100万个连接的套接字

    3)调用epoll_wait收集发生的事件的连接

    如此一来,要实现上面说是的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。

     

    下面来看看Linux内核具体的epoll机制实现思路。

    当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:

    [cpp] view plain copy
    1. struct eventpoll{  
    2.     ....  
    3.     /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/  
    4.     struct rb_root  rbr;  
    5.     /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/  
    6.     struct list_head rdlist;  
    7.     ....  
    8. };  

    每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。

    而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。

    在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示:

    [cpp] view plain copy
    1. structepitem{  
    2.     structrb_node  rbn;//红黑树节点  
    3.     structlist_head    rdllink;//双向链表节点  
    4.     structepoll_filefd  ffd;  //事件句柄信息  
    5.     structeventpoll *ep;    //指向其所属的eventpoll对象  
    6.     structepoll_eventevent;//期待发生的事件类型  
    7. }  

    当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

    epoll数据结构示意图

    从上面的讲解可知:通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。

    OK,讲解完了Epoll的机理,我们便能很容易掌握epoll的用法了。一句话描述就是:三步曲。

    第一步:epoll_create()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。

    第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。

    第三部:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。

    /*************************************************************************	
      > Description:基于epoll的回声服务器端
     ************************************************************************/
    
    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    #include<unistd.h>
    #include<arpa/inet.h>
    #include<sys/socket.h>
    #include<sys/epoll.h>
    
    void error_handling(char *message);
    
    #define BUFF_SIZE 100
    #define EPOLL_SIZE 30
    
    int main(int argc, char *argv[])
    {
    	int sock_server;
    	int sock_client;
    
    	struct sockaddr_in addr_server;
    	struct sockaddr_in addr_client;
    	socklen_t size_addr_client;
    
    	char buff[BUFF_SIZE];
    	int str_len, i;
    
    	int epfd, count_event;
    	struct epoll_event *ep_events;
    	struct epoll_event event;
    	
    
    	if(argc!=2){ //命令行中启动服务程序仅限一个参数:端口号
    		printf("Usage : %s <port>\n", argv[0]);
    		exit(1);
    	}
    	
    	//调用socket函数创建套接字
    	sock_server = socket(PF_INET, SOCK_STREAM, 0);
    	if(-1 == sock_server){
    		error_handling("socket() error.");
    	}
    
    	memset(&addr_server, 0, sizeof(addr_server));
    	addr_server.sin_family = AF_INET;
    	addr_server.sin_addr.s_addr = htonl(INADDR_ANY);
    	addr_server.sin_port = htons(atoi(argv[1]));
    	
    	//调用bind函数分配IP地址和端口号
    	if( -1 == bind( sock_server, (struct sockaddr*)&addr_server, 
    				sizeof(addr_server)) ){
    		error_handling("bind() error");
    	}
    
    	//监听端口的连接请求,连接请求等待队列size为5
    	if( -1 == listen(sock_server, 5) ){
    		error_handling("listen() error");
    	}
    
    	//epoll
    	epfd = epoll_create(EPOLL_SIZE);
    	//epfd = epoll_create(0); //epoll_wait() Error
    	ep_events = (struct epoll_event*)malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
    
    	event.events = EPOLLIN;//监视需用读取数据事件
    	event.data.fd=sock_server;
    	epoll_ctl(epfd, EPOLL_CTL_ADD, sock_server, &event);
    	//
    	while(1){
    		count_event = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
    		if(count_event == -1){
    			puts("epoll_wait() Error");
    			break;
    		}
    
    		for(i=0; i<count_event; i++){
    			if(ep_events[i].data.fd == sock_server){
    				//接受连接请求
    				size_addr_client = sizeof(addr_client);
    				sock_client = accept( sock_server, (struct sockaddr*)&addr_client, &size_addr_client);
    				event.events = EPOLLIN;
    				event.data.fd = sock_client;
    				epoll_ctl(epfd, EPOLL_CTL_ADD, sock_client, &event);
    				printf("Connected client : %d\n", sock_client);
    			}else{
    				str_len = read(ep_events[i].data.fd, buff, BUFF_SIZE);
    				if(str_len){//echo to client
    					buff[str_len] = 0;
    					printf("Message from client %d: %s", i, buff);
    					write(ep_events[i].data.fd, buff, str_len);//echo!
    				}else{ //close connection
    					epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
    					close(ep_events[i].data.fd);
    					printf("Disconnected client %d!\n", ep_events[i].data.fd);
    				}
    			}//end of if()
    		}//end of while
    	}//end of for
    
    	//断开连接,关闭套接字
    	close(sock_server);
    	close(epfd);//
    
    	return 0;
    }
    
    void error_handling(char *message)
    {
    	fputs(message, stderr);
    	fputc('\n', stderr);
    	exit(EXIT_FAILURE);
    }
    


    展开全文
  • 在介绍多进程并发模型前,先看看之前的一个TCP socket 例子,一个同步阻塞迭代模型。其服务器端核心代码如下,完整代码参见前面链接, 同步阻塞迭代服务器模型:

    在介绍多进程并发模型前,先看看之前的一个TCP socket 例子,一个同步阻塞迭代模型。其服务器端核心代码如下,完整代码参见前面链接,

    同步阻塞迭代服务器模型:

    ser_sockfd = socket(…);
    bind(ser_sockfd,…);
    listen(ser_sockfd,…);
    while(1)
    {
    	cli_sockfd = accept(ser_sockfd,…);	
    	//recv(cli_sockfd,buf,…);
    	read(cli_sockfd,buf,…);
    	doit(buf);
    	//send(cli_sockfd,buf,…);
    	write(cli_sockfd,buf,…);
    }
    上面程序大致存在这么一些弊端,基本上都在阻塞阶段:
    1. 我们知道如果客户端没有发来连接请求,那么服务器端进程将会阻塞在 accept 系统调用处,程序而不能执行其他任何操作,直到有客户端调用 connect 发送连接请求,accept 返回,程序才能进行后面的操作。
    2. 在与客户端建立连接之后,就可以进行正常通信了,这里服务器端通过 read 系统调用从客户端接收数据,对于read 函数,如果客户端迟迟不发送数据过来,那么程序同样也会阻塞在 read 调用,苦等 read 返回,这时,如果还有另外的客户端请求连接时,都会失败。
    3. 另外同样的道理,write 系统调用也会使得程序出现阻塞(常见的就是客户端写缓冲区满了),write函数将苦等到系统缓冲区有足够的空间把你要发送的数据拷进去才返回。

    阻塞的结果就是一直在耗费系统资源,前后就一个进程在运行,什么事情都仰仗着它来完成,一旦阻塞就得等着它完成这件事才能干后面的事情,耗费系统资源。 另外更重要的是当一个客户请求传输花费较长时间时,服务器将被单个客户长时间占用,如前面的迭代模型(前面链接)设置的是单个用户一直占用,这样服务器不可能同时服务多个客户,必须等这个终止了才行。

    这样我们可以考虑 fork 一个子进程来服务每个客户:当一个连接建立时,accept 返回,服务器接着调用 fork,然后子进程服务客户(通过已连接套接口 cli_sockfd),父进程则等待另一个连接(通过监听套接口 listenfd)。其中父进程关闭已连接套接口,负责监听外来客户连接请求,子进程则关闭监听套接口,负责与服务器端的数据通信。因为我们fork 子进程是在 accept 返回之后,此时连接已经建立,监听套接口和已连接套接口都在父进程与子进程之间共享。

    看代码,这个程序是根据前面同步阻塞迭代模型修改而来,while循环内不断读取写入,服务器端会堵塞在recv处(客户端程序):

    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    #include<sys/socket.h>
    #include<sys/types.h>
    #include<unistd.h>
    #include<netinet/in.h>
    #include <errno.h>  
    #define PORT 6666
    
    int main(int argc,char **argv)
    {
    	pid_t pid;
    	int ser_sockfd,cli_sockfd;
    	int err,n;
    	int addlen;
    	struct sockaddr_in ser_addr;
    	struct sockaddr_in cli_addr;
    	char recvline[200],sendline[200];
    	
    	ser_sockfd = socket(AF_INET,SOCK_STREAM,0);          //创建套接字
    	if(ser_sockfd == -1)
    	{
    		printf("socket error:%s\n",strerror(errno));
    		return -1;
    	}
    	
    	bzero(&ser_addr,sizeof(ser_addr));
    	
    	/*在待捆绑到该TCP套接口(sockfd)的网际套接口地址结构中填入通配地址(INADDR_ANY)
    	和服务器的众所周知端口(PORT,这里为6666),这里捆绑通配地址是在告知系统:要是系统是
    	多宿主机(具有多个网络连接的主机),我们将接受宿地址为任何本地接口的地址*/     
    	ser_addr.sin_family = AF_INET;
    	ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    	ser_addr.sin_port = htons(PORT);
    	
    	//将网际套接口地址结构捆绑到该套接口
    	err = bind(ser_sockfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr));  
    	if(err == -1)
    	{
    		printf("bind error:%s\n",strerror(errno));
    		return -1;
    	}
    	//将套接口转换为一个监听套接口,监听等待来自客户端的连接请求
    	err = listen(ser_sockfd,5);                                      
    	if(err == -1)
    	{
    		printf("listen error\n");
    		return -1;
    	}
    	
    	printf("listen the port:\n");
    	
    	while(1)
    	{	
    		addlen = sizeof(struct sockaddr);
    		//等待阻塞,等待客户端申请,并接受客户端的连接请求
    		//accept成功,将创建一个新的套接字,并为这个新的套接字分配一个套接字描述符
    		cli_sockfd = accept(ser_sockfd,(struct sockaddr *)&cli_addr,&addlen);   
    		if(cli_sockfd == -1)
    		{
    			printf("accept error\n");
    		}
    		if((pid = fork()) == 0)
    		{
    		//数据传输
    			close(ser_sockfd);
    			while(1)
    			{
    				printf("waiting for client...\n");
    				n = recv(cli_sockfd,recvline,1024,0);
    				if(n == -1)
    				{
    					printf("recv error\n");
    				}
    				recvline[n] = '\0';
    			
    				printf("recv data is:%s\n",recvline);
    				
    				printf("Input your words:");
    				scanf("%s",sendline);
    				send(cli_sockfd,sendline,strlen(sendline),0);
    			}
    		}
    		close(cli_sockfd);
    	}
    	close(cli_sockfd);
    	
    	return 0;
    }
    

    精练一下,看看核心代码部分(上面是每个客户一直与服务器端进行通信,数据传输采用recv和send,flag标志位置0,和read,write无异,下面修改一下采用执行一次数据交互,类似于客户端发过来数据,服务器接收到这个数据进行其余操作,再反馈给客户,通信终止)

    多进程并发服务器模型:

    ser_sockfd = socket(…);
    bind(ser_sockfd,…);
    listen(ser_sockfd,…);
    while(1)
    {
    	cli_sockfd = accept(ser_sockfd,…);	
    	pid = fork();
    	if(-1 == pid)
    		do_err_handler();
    	if(0 == pid)//child
    	{
    			close(ser_sockfd);//监听套接口
    			//recv(cli_sockfd,buf,…);
    			read(cli_sockfd,buf,…);
    			doit(buf);
    			//send(cli_sockfd,buf,…);
    			write(cli_sockfd,buf,…);
    			close(cli_sockfd);
    	}
    	close(cli_sockfd);//parent
    }
    同前面同步迭代模型一样,也会有阻塞在 accept 的可能性,但是较为改进的是:一旦某个客户端连接建立起来,会立即 fork 一个新的进程来处理与这个客户的数据交互,避免程序阻塞在read调用处,而影响其他客户端的连接。看程序,父进程负责监听客户端的连接请求,子进程负责数据交互。分工合作,父进程一直阻塞在accept 处等待新客户的连接请求,已连接的与客户之间的数据交互则由子进程来完成。

    为做对比,这里贴出迭代模型的服务器端代码,客户端代码点此

    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    #include<sys/socket.h>
    #include<sys/types.h>
    #include<unistd.h>
    #include<netinet/in.h>
    #include <errno.h>  
    #define PORT 6666
    
    int main(int argc,char **argv)
    {
    	int ser_sockfd,cli_sockfd;
    	int err,n;
    	int addlen;
    	struct sockaddr_in ser_addr;
    	struct sockaddr_in cli_addr;
    	char recvline[200],sendline[200];
    	
    	ser_sockfd = socket(AF_INET,SOCK_STREAM,0);          //创建套接字
    	if(ser_sockfd == -1)
    	{
    		printf("socket error:%s\n",strerror(errno));
    		return -1;
    	}
    	
    	bzero(&ser_addr,sizeof(ser_addr));
        
    	ser_addr.sin_family = AF_INET;
    	ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    	ser_addr.sin_port = htons(PORT);
    	
    	//将网际套接口地址结构捆绑到该套接口
    	err = bind(ser_sockfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr));  
    	if(err == -1)
    	{
    		printf("bind error:%s\n",strerror(errno));
    		return -1;
    	}
    	//将套接口转换为一个监听套接口,监听等待来自客户端的连接请求
    	err = listen(ser_sockfd,5);                                      
    	if(err == -1)
    	{
    		printf("listen error\n");
    		return -1;
    	}
    	
    	printf("listen the port:\n");
    	
    	while(1)
    	{	
    		addlen = sizeof(struct sockaddr);
    		//等待阻塞,等待客户端申请,并接受客户端的连接请求
    		//accept成功,将创建一个新的套接字,并为这个新的套接字分配一个套接字描述符
    		cli_sockfd = accept(ser_sockfd,(struct sockaddr *)&cli_addr,&addlen);   
    		if(cli_sockfd == -1)
    		{
    			printf("accept error\n");
    		}
    		
    		//数据传输
    		printf("waiting for client...\n");
    		n = recv(cli_sockfd,recvline,1024,0);
    		if(n == -1)
    		{
    			printf("recv error\n");
    		}
    		recvline[n] = '\0';
    		
    		printf("recv data is:%s\n",recvline);
    		
    		printf("Input your words:");
    		scanf("%s",sendline);
    		send(cli_sockfd,sendline,strlen(sendline),0);
    
    		close(cli_sockfd);
    	}
    	close(ser_sockfd);
    	
    	return 0;
    }
    
    上面这个同步迭代服务器模型,如果一个客户正在与该服务器进行数据传输,或阻塞在某处(recv),此时当新客户试图与该服务器建立连接时,便不能得到及时响应,必须等服务器与老客户完成通信后,服务器才能与新客户连接进行数据通信。而这里讲的多进程服务器模型则不一样,与客户进行数据通信的都是同一个父进程fork出来的子进程,父进程则负责监听,有新客户再fork一个子进程与之数据通信,彼此不干扰,不会影响到新客户的连接。

    注意到上面这个多进程并发模型,每次有客户请求连接时,父进程又会fork 一个子进程(阻塞在accept处,监听新客户连接请求的一直是最开始的那个父进程),换句话说,服务器有多少个客户与之连接,期间就要fork 多少个子进程来进行数据交互。虽然Linux 在创建进程中采用写时拷贝机制,大大降低了fork 一个子进程的消耗,但若客户端连接较大,一个服务器有大量的客户连接是很正常的事情,这时系统仍然将不堪重负。


    我们可以通过多线程(线程池)并发模型,在一定程序上改善这个问题。






    展开全文
  • I/O复用主要用于网络应用,典型使用在一下场合: 1.当客户处理多个描述字(通常是交互式输入和网络套接口)时,必须使用I/O复用; 2.一个客户同时处理多个套接口时; 3.如果一个TCP服务器既要处理监听套接口,又要...
  • 5种服务器网络编程模型讲解

    千次阅读 2015-06-11 23:10:41
    高并发网络编程之epoll详解 中对epoll相关实现已经有详细解决,这里就直接摘录过来。 由于epoll的实现机制与select/poll机制完全不同,上面所说的 select的缺点在epoll上不复存在。 设想一下如下场景:有100...
  • 1、服务器端IdTCPServer1->Active=false;断开连接后,客户端能否得知? 2、客户端连接到服务器后,TIdTCPClient有没有一个属性判断是否已连接? 3、用TIdNotify::NotifyMethod()编译时警告如下,有没有替代的...
  • 几种典型的服务器网络编程模型归纳(select poll epoll)

    千次阅读 多人点赞 2016-12-17 10:52:17
    高并发网络编程之epoll详解 由于epoll的实现机制与select/poll机制完全不同,上面所说的 select的缺点在epoll上不复存在 。 设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻...
  • 1)服务器端代码main函数#include "unp.h" int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; liste....
  • Windows服务器网络编程 Linux服务器网络编程 https://ke.qq.com/webcourse/index.html?client=tim#cid=131973&term_id=100147409&taid=3391512335483781&client=tim 转载于:...
  • 网络游戏服务器编程 part 6网络游戏服务器编程 part 6 网络游戏服务器编程 part 6网络游戏服务器编程 part 6 网络游戏服务器编程 part 6网络游戏服务器编程 part 6
  • 网络编程--服务器编程模型

    千次阅读 2012-05-22 09:47:33
    本文通过一个简单的例子,介绍网络服务器编程模型 服务器接受客户端连接请求,回显客户端发过来的数据,发送当前时间给客户端 所有源码可打包下载: http://download.csdn.net/detail/yfkiss/4318990 客户端...
  • 网络编程】Socket网络编程基础

    千次阅读 2019-06-12 21:41:50
    文章目录网络编程概述Socket与TCP UDPSocket TCP演示报文段协议Mac地址IP、端口、远程服务器IPv4IPv6端口远程服务器 网络编程概述 什么是网络编程 网络编程从大的方面说就是对信息的发送到接收 通过操作相应Api调度...
  • 1.2 网络编程技术 ...按照前面的基础知识介绍,无论使用TCP方式还是UDP方式进行网络通讯,网络编程都是由客户端和服务器端组成。当然,B/S结构的编程中只需要实现服务器端即可。所以,下面介绍网络编程的步骤时...
  • Linux网络编程[UDP客户端服务器的编程模型] 编程模型概述 相关函数 实例demo 编程模型概述从一个图示开始: 从上述图示中我们都可以看到,UDP的传输相对来说比TCP传输的时候要简单很多,因为其不需要建立稳定连接,...
  • 一、服务器编程框架 模块 单个服务器程序 服务器机群 I/O处理单元 处理客户连接,读写网络数据 作为接入服务器,实现负载均衡 逻辑单元 业务进程或线程 逻辑服务器 网络存储单元 本地...
  • TCP网络编程服务器多线程实现   1、TCP网络编程服务器多线程实现的背景 (1)假设我们的一个服务器供很多客户端使用,而这些客户端都是来上传文件的。那么,如果服务器端是单线程实现的,则就会出现”先到的...
  • 这些年,接触了形形色色的项目,写了不少网络编程的代码,从windows到linux,跌进了不少坑,由于网络编程涉及很多细节和技巧,一直想写篇文章来总结下这方面的心得与经验,希望对来者有一点帮助,那就善莫大焉了。...
  • Socket网络编程 连接邮箱服务器+授时服务器

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 28,179
精华内容 11,271
关键字:

服务器网络编程