美文网首页
计算机基础知识——linux socket套接字udp连接分析

计算机基础知识——linux socket套接字udp连接分析

作者: zuoerfeng | 来源:发表于2018-11-06 17:08 被阅读0次

    2016.7.5

    今天早上对项目顶层文件(daemon.c)进行了分析,对其中的UDP连接进行了具体代码级分析。

    1、需求分析

    同样,首先我们得知道用UDP的需求分析,从昨天的分析中知道UDP支持数据量小,不支持可靠服务的传输,从项目文档“测试机程序结构”分析可以知道,接收服务器端下发的命令是用的UDP,执行测试的结果最后也是以UDP的形式发送给服务器。同时还要监听测试结果进程,将测试结果发送给套接字中。

    个人理解上来看,执行最后的测试结果有应该是一个大文件,并且传输的时候应该保证有效性,应该选用TCP连接,这里是个疑问?

    2、UDP连接原理分析

    前面已经讲述了socket套接字的说明,包括其数据结构是怎么样的,形象的比喻就是:socket就是一个口袋,一个洞,用户可以通过这个洞直接与网络协议栈打交道,完成网络通信,基于它就是因为抽象接口封装,简单。这里就直接给出了UDP连接的通信流程:

    典型的UDP客户端/服务器通讯过程如下图所示:

    从这个图上可以看出,由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,实际上有很多保证通讯可靠性的机制需要在应用层实现,可能反而会需要更多代码。

    可以看到,我们的测试机仍然是服务器端,服务器端比直接TCP的程序流程要简单,没有监听listen()这个动作了,直接调用recvfrom()函数对端口号的监听,这个函数没有数据到来时候一直阻塞,有请求后就知道通过地址,知道数据来自哪个端口号,从而判断是什么数据。

    3、UDP连接代码分析(需要说明的是:这里以项目的daemon.c代码为例 )

    按照上述的服务器端的流程,我们一步步按照流程进行代码级的分析:

    (1)定义一个socket套接字

    定义套接字时候会要传入对应的参数,如上图所示,对套接字的数据结构的说明,在之前的TCP分析中已经有说明了。

    (2)bind绑定套接字的地址和端口号

    bind的用法和TCP连接一样,没有什么区别。

    (3)recvfrom阻塞等待客户端请求

    recvfrom()函数的原型解释:

    recvfrom()
    简述:
      接收一个数据报并保存源地址。
      #include <winsock.h>
      int PASCAL FAR recvfrom( SOCKET s, char FAR* buf, int len, int flags,
      struct sockaddr FAR* from, int FAR* fromlen);
      s:标识一个已连接套接口的描述字。
      buf:接收数据缓冲区。
      len:缓冲区长度。
      flags:调用操作方式。
      from:(可选)指针,指向装有源地址的缓冲区。
      fromlen:(可选)指针,指向from缓冲区长度值。
    

    注释:

    本函数由于从(已连接)套接口上接收数据,并捕获数据发送源的地址。

    对于SOCK_STREAM类型的套接口,最多可接收缓冲区大小个数据。如果套接口被设置为线内接收带外数据(选项为SO_OOBINLINE),且有带外数据未读入,则返回带外数据。应用程序可通过调用ioctlsocket()的SOCATMARK命令来确定是否有带外数据待读入。对于SOCK_STREAM类型套接口,忽略from和fromlen参数。

    对于数据报类套接口,队列中第一个数据报中的数据被解包,但最多不超过缓冲区的大小。如果数据报大于缓冲区,那么缓冲区中只有数据报的前面部分,其他的数据都丢失了,并且recvfrom()函数返回WSAEMSGSIZE错误。

    若from非零,且套接口为SOCK_DGRAM类型,则发送数据源的地址被复制到相应的sockaddr结构中。fromlen所指向的值初始化时为这个结构的大小,当调用返回时按实际地址所占的空间进行修改。

    如果没有数据待读,那么除非是非阻塞模式,不然的话套接口将一直等待数据的到来,此时将返回SOCKET_ERROR错误,错误代码是WSAEWOULDBLOCK。用select()或WSAAsynSelect()可以获知何时数据到达。

    如果套接口为SOCK_STREAM类型,并且远端“优雅”地中止了连接,那么recvfrom()一个数据也不读取,立即返回。如果立即被强制中止,那么recv()将以WSAECONNRESET错误失败返回。

    在套接口的所设选项之上,还可用标志位flag来影响函数的执行方式。也就是说,本函数的语义既取决于套接口选项,也取决于标志位参数。标志位可取下列值:

    值意义:

    MSG_PEEK 查看当前数据。数据将被复制到缓冲区中,但并不从输入队列中删除。
    MSG_OOB 处理带外数据(参见2.2.3节具体讨论)。

    返回值:

    若无错误发生,recvfrom()返回读入的字节数。如果连接已中止,返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。

    错误代码:
    WSANOTINITIALISED:在使用此API之前应首先成功地调用WSAStartup()。
    WSAENETDOWN:WINDOWS套接口实现检测到网络子系统失效。
    WSAEFAULT:fromlen参数非法;from缓冲区大小无法装入端地址。
    WSAEINTR:阻塞进程被WSACancelBlockingCall()取消。
    WSAEINPROGRESS:一个阻塞的WINDOWS套接口调用正在运行中。
    WSAEINVAL:套接口未用bind()进行捆绑。
    WSAENOTCONN:套接口未连接(仅适用于SOCK_STREAM类型)。
    WSAENOTSOCK:描述字不是一个套接口。
    WSAEOPNOTSUPP:指定了MSG_OOB,但套接口不是SOCK_STREAM类型的。
    WSAESHUTDOWN:套接口已被关闭。当一个套接口以0或2的how参数调用shutdown()关闭后,无法再用recv()接收数据。
    WSAEWOULDBLOCK:套接口标识为非阻塞模式,但接收操作会产生阻塞。
    WSAEMSGSIZE:数据报太大无法全部装入缓冲区,故被剪切。
    WSAECONNABORTED:由于超时或其他原因,虚电路失效。
    WSAECONNRESET:远端强制中止了虚电路。

    看实例代码中是如何运用的:

    recvfrom操作自带有阻塞功能,当没有接受到请求的时候自己阻塞,等待请求的到来,接受到了请求,同时接受请求 的数据,将数据放到buf中,因为udp是少数据流的协议控制,所以说很少有不能一次copy所有的情况,所以传来的数据直接到buf里面即可,后面的工作就是对buf的数据进行相应的操作了。

    ret是recv的返回值,表示接受数据的大小。

    前面讨论过我们的测试系统实际上的udp的套接字完成了两个端口的监听,第一个服务器端的监听,第二个是用本地通信端口的监听,所以在实际代码中还有另外一个套接字,完成的也是上述的定义、绑定初始化,然后进行recv监听,代码如下:

    分析和上面的调用一样,没有什么好分析的。

    (4)sendto()发送数据给客户端

    在我们系统测试完整个操作之后,测试结果通过UDP本地进程的通信发送给UDP的buf中,然后在通过UDP连接从buf中把数据发送到服务器上,完成结果的交付工作。

    那么这样存在一个问题,他们的buf是不是一样的???会不会有重叠的问题出现????

    select函数对max_sd套接字进行可读性的监听,所以任何时候可以同时监听到这三个套接字。但是我们的代码是顺序执行的,buf是共用的,buf首先会给tcp使用,再然后给udp使用,每个使用过程中,会最后将buf下发下去,数据也就没有用了。之后在给下一个用。

    所以对于udp来说,udp如果监听到了msg套接字的连接请求,相应后将数据放到对应的buf中,然后这个时候才建立一个新的socket,这个套接字用于想服务器传送之前的数据,但是这个时候其实我们是知道服务器的地址的端口号的,不需要在进行新的绑定了,建立了这个心的socket,直接向服务器端发送该数据即可。

    首先定义一个传输的新套接字:

    sockfd = socket(AF_INET, SOCK_DGRAM, 0)
    

    SOCK_DGRAM是无保障的面向消息的socket,主要用于在网络上发广播消息。

    两个重要的类型是SOCK_STREAM和SOCK_DGRAM。SOCK_STREAM表明数据向字符流一样通过socket,但是SOCK_DGRAM则表明数据是以数据报的形式通过socket的

    这里定义的套接字规定了其发送的数据是数据包的形式,证明了是一个包,对方需要对这个包解析,有头部的。

    然后进行设备套接字选项,设备此套接字可以重用本地的端口号和地址:

    setsockpt()函数说明如下:

    int setsockopt (
      SOCKET s,                
      int level,              
      int optname,             
      const char FAR * optval, 
      int optlen               
    );
    

    The Windows Sockets setsockopt function sets a socket option.
    中文解释好像是:设置套接字的选项。
    先看如下代码:

    setsockopt(SockRaw,IPPROTO_IP,IP_HDRINCL,(char *)&flag,sizeof(int))
    

    这里是设置SockRaw这个套接字的ip选项中的IP_HDRINCL
    参考以下资料:


    Linux网络编程--8. 套接字选项

    有时候我们要控制套接字的行为(如修改缓冲区的大小),这个时候我们就要控制套接字的选项了.

    8.1 getsockopt和setsockopt 
    
    int getsockopt(int sockfd,int level,int optname,void *optval,socklen_t *optlen)
    int setsockopt(int sockfd,int level,int optname,const void *optval,socklen_t *optlen)
    
    level指定控制套接字的层次.可以取三种值:
    1)SOL_SOCKET:通用套接字选项.
    2)IPPROTO_IP:IP选项.
    3)IPPROTO_TCP:TCP选项. 
    
    optname指定控制的方式(选项的名称),我们下面详细解释 
    optval获得或者是设置套接字选项.根据选项名称的数据类型进行转换 
    
    选项名称        说明                  数据类型
    
    ========================================================================
    
                SOL_SOCKET
    
    ------------------------------------------------------------------------
    
    SO_BROADCAST      允许发送广播数据            int
    
    SO_DEBUG        允许调试                int
    
    SO_DONTROUTE      不查找路由               int
    
    SO_ERROR        获得套接字错误             int
    
    SO_KEEPALIVE      保持连接                int
    
    SO_LINGER        延迟关闭连接              struct linger
    
    SO_OOBINLINE      带外数据放入正常数据流         int
    
    SO_RCVBUF        接收缓冲区大小             int
    
    SO_SNDBUF        发送缓冲区大小             int
    
    SO_RCVLOWAT       接收缓冲区下限             int
    
    SO_SNDLOWAT       发送缓冲区下限             int
    
    SO_RCVTIMEO       接收超时                struct timeval
    
    SO_SNDTIMEO       发送超时                struct timeval
    
    linux下,如果一个进程帮定某个port,那当进程结束时,该port仍然会被继续占用几十秒,在这段时间内尝试对
    
    该port的绑定都会返回失败。解决方法:调用setsockopt()启用SO_REUSERADDR属性
    
    SO_REUSERADDR      允许重用本地地址和端口         int
    
    SO_TYPE         获得套接字类型             int
    
    SO_BSDCOMPAT      与BSD系统兼容              int
    
    ==========================================================================
    
                IPPROTO_IP
    
    --------------------------------------------------------------------------
    
    IP_HDRINCL       在数据包中包含IP首部          int
    
    IP_OPTINOS       IP首部选项               int
    
    IP_TOS         服务类型
    
    IP_TTL         生存时间                int
    
    ==========================================================================
    
                IPPRO_TCP
    
    --------------------------------------------------------------------------
    
    TCP_MAXSEG       TCP最大数据段的大小           int
    
    TCP_NODELAY       不使用Nagle算法             int
    
    =========================================================================
    

    详细的选项请用 man ioctl_list 查看.

    udp固定端口发送

     sockaddr_in         cliaddr;   
     cliaddr.sin_family   =   AF_INET;   
     cliaddr.sin_port   =   htons(1025);//BTW:1024到5000间的端口是系统用于自动分配的,小心了   
     cliaddr.sin_addr.s_addr   =   htonl(INADDR_ANY);   
     bind(你的客户端socket,   (sockaddr*)&cliaddr,   sizeof(cliaddr));
    

    最后想指定的端口发送对应的数据包即可,使用额是sendto函数,函数 原型如下:

    sendto()
    

    简述:

    向一指定目的地发送数据。

      #include <winsock.h>
      int PASCAL FAR sendto( SOCKET s, const char FAR* buf, int len, int flags,
      const struct sockaddr FAR* to, int tolen);
    
      s:一个标识套接口的描述字。
      buf:包含待发送数据的缓冲区。
      len:buf缓冲区中数据的长度。
      flags:调用方式标志位。
      to:(可选)指针,指向目的套接口的地址。
      tolen:to所指地址的长度。
    

    注释:

    sendto()适用于已连接的数据报或流式套接口发送数据。对于数据报类套接口,必需注意发送数据长度不应超过通讯子网的IP包最大长度。IP包最大长度在WSAStartup()调用返回的WSAData的iMaxUdpDg元素中。如果数据太长无法自动通过下层协议,则返回WSAEMSGSIZE错误,数据不会被发送。请注意成功地完成sendto()调用并不意味着数据传送到达。

    sendto()函数主要用于SOCK_DGRAM类型套接口向to参数指定端的套接口发送数据报。对于SOCK_STREAM类型套接口,to和tolen参数被忽略;这种情况下sendto()等价于send()。

    为了发送广播数据(仅适用于SOCK_DGRAM),in参数所含地址应该把特定的IP地址INADDR_BROADCAST(winsock.h中有定义)和终端地址结合起来构造。通常建议一个广播数据报的大小不要大到以致产生碎片,也就是说数据报的数据部分(包括头)不超过512字节。

    如果传送系统的缓冲区空间不够保存需传送的数据,除非套接口处于非阻塞I/O方式,否则sendto()将阻塞。对于非阻塞SOCK_STREAM类型的套接口,实际写的数据数目可能在1到所需大小之间,其值取决于本地和远端主机的缓冲区大小。可用select()调用来确定何时能够进一步发送数据。

    在相关套接口的选项之上,还可通过标志位flag来影响函数的执行方式。也就是说,本函数的语义既取决于套接口的选项也取决于标志位。后者由以下一些值组成:

    值意义

    MSG_DONTROUTE 指明数据不选径。一个WINDOWS套接口供应商可以忽略此标志;参见2.4节中关于SO_DONTROUTE的讨论。

    MSG_OOB 发送带外数据(仅适用于SO_STREAM;参见2.2.3节)。

    返回值:

    若无错误发生,send()返回所发送数据的总数(请注意这个数字可能小于len中所规定的大小)。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。

    看代码中实例:

    方框表示的就是特定的IP地址INADDR_BROADCAST(winsock.h中有定义)和终端地址结合起来构造。

    (5)关闭套接字

    close(sockfd)
    

    很简单,和之前的tcp关闭时一样的,没有什么多说的。。。。

    相关文章

      网友评论

          本文标题:计算机基础知识——linux socket套接字udp连接分析

          本文链接:https://www.haomeiwen.com/subject/kabuxqtx.html