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

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关闭时一样的,没有什么多说的。。。。

猜你喜欢

转载自blog.csdn.net/u012414189/article/details/83830961