socket 由浅入深 原理(一)
来源;MSDN 网络整理
主要介绍:CreateCompletionPort, socket、bind、listen、connect、accept、send、sendto、recv、recvfrom、close、shutdown
原理可看:
HANDLE CreateIoCompletionPort( HANDLE FileHandle, HANDLE ExistingCompletionPort, ULONG_PTR CompletionKey, DWORD NumberOfConcurrentThreads );最开始创建一个完成端口时,唯一感兴趣的参数便是NumberOfConcurrentThreads(并发线程的数量);前面三个参数都会被忽略。NumberOfConcurrentThreads参数的特殊之处在于,它定义了在一个完成端口上,同时允许执行的线程数量。理想情况下,我们希望每个处理器各自负责一个线程的运行,为完成端口提供服务,避免过于频繁的线程“场景”切换。若将该参数设为0,表明系统内安装了多少个处理器,便允许同时运行多少个线程!可用下述代码创建一个I/O完成端口:
HANDLE hIocp; hIocp = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, (ULONG_PTR)0, 0); if (hIocp == NULL) { // Error }
_________________________返回值:非负描述符 – 成功,-1 - 出错
其中:
family指明了协议族/域,通常AF_INET、AF_INET6、AF_LOCAL等;
type是套接口类型,主要SOCK_STREAM、SOCK_DGRAM、SOCK_RAW;
protocol一般取为0。成功时,返回一个小的非负整数值,与文件描述符类似。
对于windows环境下,在调用该函数之前需首先调用WSAStartup函数完成对Winsock服务的初始化,如
#include<WinSock2.h>
WSADATA wdata;
if ( WSAStartup(MAKEWORD(2,2), &wdata) !=0 ){
return INVALID_SOCKET;
}
后面即可调用socket函数,参数意义与linux环境一致。
_________________________返回值:0 – 成功,-1 - 出错
当socket函数返回一个描述符时,只是存在于其协议族的空间中,并没有分配一个具体的协议地址(这里指IPv4/IPv6和端口号的组合),bind函数可以将一组固定的地址绑定到sockfd上。
其中:
sockfd是socket函数返回的描述符;
myaddr指定了想要绑定的IP和端口号,均要使用网络字节序-即大端模式;
addrlen是前面struct sockaddr(与sockaddr_in等价)的长度。
为了统一地址结构的表示方法,统一接口函数,使得不同的地址结构可以被bind()、connect()、recvfrom()、sendto()等函数调用。但一般的编程中并不直接对此数据结构进行操作,而使用另一个与之等价的数据结构sockaddr_in。
通常服务器在启动的时候都会绑定一个众所周知的协议地址,用于提供服务,客户就可以通过它来接连服务器;而客户端可以指定IP或端口也可以都不指定,未分配则系统自动分配。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
Windows下的版本:
Int bind( IN SOCKET s, IN const struct sockaddr FAR * name, IN int namelen);
______________________返回值:0 – 成功,-1 - 出错
(截图来自:《UNIX网络编程第一卷》)
两个队列之和数量不得超过backlog.
______________________返回值:0 – 成功,-1 - 出错
通过此函数建立于TCP服务器的连接,实际是发起三次握手过程,仅在连接成功或失败后返回。参数sockfd是本地描述符,addr为服务器地址,addrlen是socket地址长度。
UDP的connect函数,结果与tcp调用不相同,没有三次握手过程。内核只是记录对方的ip和端口号,他们包含在传递给connect的套接口地址结构中,并立即返回给调用进程。
______________________返回值:非负描述符 – 成功,-1 - 出错
(截图来自:《UNIX网络编程第一卷》)
返回值:
>0 – 成功拷贝至发送缓冲区的字节数(可能小于len),
-1 – 出错,并置错误号errno.
Windows版本:
int send(IN SOCKET s, IN const char FAR * buf, IN int len, IN int flags)
失败时返回 -1/SOCKET_ERROR
其中:
sockfd:发送端套接字描述符(非监听描述符)
buf:应用要发送数据的缓存
len:实际要发送的数据长度
flag:一般设置为0
每个TCP套接口都有一个发送缓冲区,它的大小可以用SO_SNDBUF这个选项来改变。调用send函数的过程,实际是内核将用户数据拷贝至TCP套接口的发送缓冲区的过程:若len大于发送缓冲区大小,则返回-1;否则,查看缓冲区剩余空间是否容纳得下要发送的len长度,若不够,则拷贝一部分,并返回拷贝长度(指的是非阻塞send,若为阻塞send,则一定等待所有数据拷贝至缓冲区才返回,因此阻塞send返回值必定与len相等);若缓冲区满,则等待发送,有剩余空间后拷贝至缓冲区;若在拷贝过程出现错误,则返回-1。关于错误的原因,查看errno的值。
如果send在等待协议发送数据时出现网络断开的情况,则会返回-1。注意:send成功返回并不代表对方已接收到数据,如果后续的协议传输过程中出现网络错误,下一个send便会返回-1发送错误。TCP给对方的数据必须在对方给予确认时,方可删除发送缓冲区的数据。否则,会一直缓存在缓冲区直至发送成功(TCP可靠数据传输决定的)。
const struct sockaddr *dst_addr, socklen_t addrlen);
windows版本:
int sendto(IN SOCKET s, IN const char FAR * buf, IN int len, IN int flags, IN const struct sockaddr FAR * to, IN int tolen)
通常用于UDP套接口,用数据报方式进行数据传输。由于无连接的数据报模式下,没有建立连接,需指明目的地址,addrlen通常为sizeof(sockaddr)的长度。成功时返回发送的字节数,失败返回-1。
当本地与不同目的地址通信时,只需指定目的地址,可使用同一个UDP套接口描述符sockfd,而TCP要预先建立连接,每个连接都会产生不同的套接口描述符,体现在:客户端要使用不同的fd进行connect,服务端每次accept产生不同的fd。
因为UDP没有真正的发送缓冲区,因为是不可靠连接,不必保存应用进程的数据拷贝,应用进程中的数据在沿协议栈向下传递时,以某种形式拷贝到内核缓冲区,当数据链路层把数据传出后就把内核缓冲区中数据拷贝删除。因此它不需要一个发送缓冲区。写UDP套接口的sendto/write返回表示应用程序的数据或数据分片已经进入链路层的输出队列,如果输出队列没有足够的空间存放数据,将返回错误ENOBUFS.
关于TCP/UDP套接口的发送缓冲区理解:
(1)下图展示了应用进程写数据到TCP套接口的过程:
每一个TCP套接口有一个发送缓冲区,我们可以用SO_SNDBUF套接口选项来改变这个缓冲区的大小。当应用程序调用write时,内核从应用程序进程的缓冲区中拷贝所有数据到套接口的发送缓冲区。如果套接口的发送缓冲区容不下应用程序的所有数据(或是应用程序的缓冲区大于套接口发送缓冲区,或是套接口发送缓冲区还有其他数据),应用进程将被挂起(睡眠)。这里假设套接口是阻塞的,它是通常的缺省设置(还有非阻塞的套接口)。内核将不从write系统调用返回,直到应用程序缓冲区中的所有数据都拷贝到套接口发送缓冲区。因此从写一个TCP套接口的write调用成功返回仅仅表示我们可以重新使用应用进程的缓冲区。它并不告诉我们对端的TCP或应用程序已接收到数据。
TCP取套接口发送缓冲区的数据并把它发送给对端TCP,其过程基于TCP数据传送的所有规则。对端TCP必需确认收到数据,只有收到对端的ACK,本端TCP才能删除套接口发送缓冲区中已确认的数据。TCP必需保留数据拷贝直到对端确认为止。
TCP以MSS大小的或更小的块把数据传递给IP,同时给每个数据块安上一个TCP头部以构成TCP分节,其中的MSS是由对端通告的,当对端未通告时就用536这个值(IPv4的最小重组缓冲区字节数576减去IPv4头部字节20和TCP头部字节数20)。IP给每个TCP分节安上IP头部以构成IP数据报,查找其宿IP地址的路由表项以确定外出接口,然后把数据报传递给相应的数据链路。IP可能在把数据报传递给数据链路之前将其分片,不过我们已经谈到MSS选项的目的之一就是试图避免分片,而较新的实现又使用了路径MTU发现功能。每个数据链路都有一个输出队列,如果该队列已满,那么新到的分组将被丢弃,并沿协议栈向上返回一个错误,从链路层到IP层,再从IP层到TCP层。TCP将注意到这个错误,并在以后某个时刻重传相应分片。应用进程并不知道这种暂时情况。
(2)下图展示了应用进程写数据到UDP套接口的过程:
这一次我们展示的套接口发送缓冲区用虚线框,因为它并不存在。UDP套接口有发送缓冲区大小(我们可以用SO_SNDBUF套接口选项修改),不过它仅仅是写到套接口的UDP数据报的大小上限。如果应用进程写一个大于套接口发送缓冲区大小的数据包,内核将返回一个EMSGSIZE错误。既然UDP是不可靠的,它不必保存应用程序的数据拷贝,因此无需一个真正的发送缓冲区。(应用进程的数据在沿协议向下传递时,以某种形式拷贝到内核的缓冲区,然而数据链路层在送出这些数据后将丢弃该拷贝)
UDP简单地给用户数据报安上它的8个字节的头部以构成UDP数据报,然后传递给IP。IPv4或IPv6给UDP数据报安上相应的IP头部以构成IP数据报,执行路由操作确定外出接口,然后直接把数据包加入数据链路层输出队列(如果适合于MTU),或者分片后再把每个片加入数据链路层的输出队列。如果某个UDP应用进程发送大数据报,那么它比TCP应用进程更有可能分片,因为TCP会把应用数据划分成MSS大小的块,而UDP却没有对等的手段。
从写UDP套接口的write调用成功地返回表示用户写入的数据报或其所有片段已被加入数据链路层的输出队列。如果该队列没有足够的空间存放该数据报或它的某个片段,内核通常将给应用程序返回一个ENOBUFS错误。
其中:
sockfd:接收端套接字描述符;
buf:指定缓冲区地址,用于存储接收数据;
len:指定的用于接收数据的缓冲区长度;
flags:一般指定为0
表示从接收缓冲区拷贝数据。成功时,返回拷贝的字节数,失败返回-1。阻塞模式下,recv/recvfrom将会阻塞到缓冲区里至少有一个字节(TCP)/至少有一个完整的UDP数据报才返回,没有数据时处于休眠状态。若非阻塞,则立即返回,有数据则返回拷贝的数据大小,否则返回错误-1,置错误码为EWOULDBLOCK。
recvfrom
ssize_t recvfrom(int sockfd,void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen)
windows版本:
int recvfrom(IN SOCKET s, OUT char FAR * buf, IN int len, IN int flags, OUT struct sockaddr FAR * from,IN OUTint FAR * fromlen)
着重强调参数:
sockfd:接收端套接字描述
buf:用于接收数据的应用缓冲区地址
len:指名缓冲区大小
flags:通常为0
src_addr:数据来源端的地址
addrlen:src_addr地址的长度
注意后两个参数是输出参数,其中addrlen既是输入又是输出参数,即值-结果参数,需要在调用时,指明src_addr的长度。另外,如果不关心数据发送端的地址,可以将后两者均设置为NULL。
close缺省功能是将套接字作“已关闭”标记,并立即返回到调用进程,该套接字描述符不能再为该进程所用:即不能作为read和write(send和recv)的参数,但是TCP将试着发送发送缓冲区内已排队待发的数据,然后按正常的TCP连接终止序列进行操作(断开连接4次握手-以FIN为首的4个TCP分节)。
shutdown不仅可以灵活控制关闭连接的读、写或读写功能,而且会立即执行相应的断开动作(发送终止连接的FIN分节等),此时不论有多少进程共享此套接字描述符,都将不能再进行收发数据。
Socket 由浅入深 简单实现编程(三)