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