TCP套接口函数
简介
TCP(Transmission Control Protocol)为应用程序提供的是可靠的数据传输服务
- 文件传输协议(FTP)
- 超文本传输协议(HTTP)
- 邮局协议(POP3)
TCP连接上传输的都是字节流,它不对数据做任何解释,只保证数据有序、可靠地到达目的主机。
TCP是全双工的,一个方向停止了工作,另一个方向还可以继续传输数据。
说明:
- TCP是操作系统中一个必备的模块,是作为操作系统的一部分实现的。
- 使用TCP通信的两个应用程序不是对等的,一个作为服务器、另一个作为客户端
- 如果服务器还没有运行,客户就已经来了,客户端的连接就会失败,错误码是端口不可达
-
服务端至少有两个socket
- 一个是侦听socket,等待客户来建立连接
- 其余的是接收到客户的连接请求而创建的新的socket,用于与客户交互
-
客户端调用connect导致TCP协议开始三次握手,向服务器发送SYN,SYN到达服务器,服务器的TCP检查客户要求的服务,创建一个新的socket为客户服务,并发送SYN和ACK确认客户的SYN,客户端收到SYN和ACK后,发送ACK完成三次握手
通信流程:
connect
connect函数最主要的功能就是建立起客户与服务器的连接
客户端调用connect发起主动连接,TCP协议开始三次握手过程。三次握手完成connect才会返回成功,双方就可以在这个连接上交换数据了。
连接建立过程中,如果调用了send发送数据,这些数据会暂时保留在本地TCP缓冲区队列中,connect成功后才发送。
int WSAAPI connect(SOCKET s, const struct sockaddr FAR*name,int namelen);
- 成功返回0;失败返回SOCKET_ERROR,应用程序可以调用WSAGetLastError()得到具体的错误码。
- name: 套接口地址结构指针,包含服务器的IP地址和端口号
说明:
-
在connect之前不必非得调用bind,如果没有调用系统会帮助选择源IP地址和临时端口号;
调用bind时,发出数据报中的源IP地址和端口号就用程序指定的。
-
connect既可用于TCP也可用于UDP套接口。
对于TCP类型的套接口,它启动了三次握手过程,报文交换需要一段时间,调用connect不会立即得到结果,只有连接成功或发生错误时才会返回,通常的错误有下面几种情况。
-
连接被拒绝:WSAECONNREFUSED, 通常是由于没有进程在指定的端口上侦听,或者是由于一些其他原因,如安全控制等,不允许服务器接受到达的连接。
-
目的不可达:WSAENETUNREACH、WSAEHOSTUNREACH,这是因为客户发出的连接请求SYN在路由过程中无法到达目的主机,在中间路由器引发了ICMP不可达错误。 协议对这种错误的处理通常是保存此错误码,并按照协议规定的时间间隔继续发送SYN,如果错误恢复了,能够与服务器建立连接,就抛弃错误码;如果超时还无法连接成功,就向程序返回此错误。
但一收到ICMP目的不可达就放弃连接是不正确的,因为这种情况可能是暂时的
-
超时:WSAETIMEDOUT, 连续发送SYN,但一直收不到服务器的确认ACK,标准实现的超时是75秒,收不到对方的ACK,便返回超时错误。
-
-
在一个阻塞socket上,返回值立即指明了成功或者失败。
对于非阻塞socket,如果连接不能立即完成,connect返回SOCKET_ERROR,程序调用WSAGetLastError将返回WSAEWOULDBLOCK,应用程序可以通过下面的情况来判断什么时候连接成功了:
- 通过select函数,如果连接成功,socket将是可写的;
- 如果应用程序使用了WSAAsyncSelect,并且指明对连接事件感兴趣,当连接完成时会收到FD_CONNECT通知
-
当已经连接的socket中断时,为了返回到一个稳定的状态,程序应该重新创建socket,但如果错误码是WSAECONNREFUSED、WSAENETUNREACH或WSAETIMEDOUT时,应用程序可以再次调用connect函数
-
UDP套接字上调用connect,并不真正与对方建立连接,只是把对方的地址和端口保存到控制块中,设置了默认的目的地址,在随后的send或recv中使用,不与目的地址匹配的数据报将被扔掉。如果目的地址是全0,将断开连接,目的地址是没有明确说明的,send或recv调用将返回WSAENOTCONN,但仍然可以使用sendto或recvfrom函数。
即使socket已经连接,也可以再次调用connect改变目的地址,如果本次地址与之前的地址不同,在队列中接收到的数据报将被抛弃。
listen
listen是只能由服务器端使用的函数,适用于面向连接的套接口
需要在socket、bind之后,accept函数之前调用
刚创建的套接口默认是主动模式,函数listen把一个未连接的套接口转变成被动模式,告诉系统在这个套接口上接受连接请求,套接口所对应的TCP控制块也从CLOSED状态转变到LISTEN状态。
int WSAAPI listen(SOCKET s, int backlog);
- 成功返回0;失败返回SOCKET_ERROR,应用程序可以调用WSAGetLastError()得到具体的错误码。
- WinSock2.h中,把SOMAXCONN定义为0x7fffffff,当backlog等于SOMAXCONN时,底层协议的提供者把backlog设置为一个最大的“合理”值。
说明:
- 当一个客户到达时,如果队列已满,WinSock规范描述中说客户将收到连接错误WSAECONNREFUSED。
- 在FreeBSD中,把处于SYN_RCVD( 已经发送了SYN/ACK,等待ACK) 状态和在ESTABLISHED状态但还没有被应用程序调用accept接受的连接都算做是“待处理连接”。
accept
accept适用于面向连接的套接口,也是只能由服务器使用的函数,紧跟在listen函数调用之后。
返回已完成连接队列中的第一个连接所对应的套接口描述符,如果已完成连接队列为空 ,默认情况下,阻塞调用进程,直到有客户与服务器建立了连接才返回给调用者。
SOCKET WSAAPI accept(SOCKET s,struct sockaddr FAR * addr,int FAR * addrlen);
-
成功返回类型为SOCKET的描述符,是已经被接受的连接所对应的套接口;失败返回INVALID_SOCKET,应用程序可以调用WSAGetLastError()得到具体的错误码。
-
addr,可选的参数,指向缓冲区,用于接收连接实体的地址,地址格式由创建套接口时指定的地址簇来确定
参数addr是结果参数,保存连接实体的地址。
-
addrlen,可选参数,指向整数的指针,包含了addr缓冲区的长度。
addrlen是值/结果参数,传入时是addr缓冲区的长度,返回后包含了地址addr的实际长度。
如果addr和addrlen都为NULL或者有一个为NULL,系统不会把已连接套接口对端的地址信息返回给调用进程。
说明:
- 已连接套接口是系统为每个被接受的客户新创建的套接口,客户和服务器之间完成了三次握手,由已连接套接口处理来自客户的请求,交互完成就把已连接套接口关闭。
-
TCP建立连接过程
客户端调用connect时,向服务器发送SYN,服务器收到SYN,为进入的连接创建新的套接口,把它添加到未完成连接队列中,向客户发送SYN和ACK。
客户收到SYN和ACK,向服务器发送ACK,connect函数返回,表明成功建立连接。
ACK一旦到达服务器就完成了三次握手,服务器把未完成连接转移到已完成连接队列中,套接口的状态也从SYN_RCVD转变到ESTABLISHED,并唤醒服务进程,accept返回。
send
函数send在一个已经连接的套接口上向对方发送数据。
使用TCP的套接口,发送的数据没有限制,UDP套接口每次发送的数据不能超过底层协议最大数据报的大小,如果数据超过了底层允许的最大数据报,函数将失败,错误码为WSAEMSGSIZE,数据不被传输。
调用getsockopt,用选项SO_MAX_MSG_SIZE能够得到可以发送的最大数据报长度。
IP首部20、UDP首部8
unsigned int msg_size = 0;
int opt_len = sizeof(msg_size);
getsockopt(s, SOL_SOCKET, SO_MAX_MSG_SIZE, (char *)&msg_size, &opt_len);
int WSAAPI send(SOCKET s,const char FAR*buf,int len,int flags);
-
成功返回发送数据的长度,可能小于参数len的长度;失败返回SOCKET_ERROR,应用程序可以调用WSAGetLastError()得到具体的错误码。
-
buf,包含了要发送的数据;
-
len,buf中数据的长度;
-
flag,规定了调用方式, 影响send函数的行为,可能是0或者下表中一个或者多个常量值的逻辑或
-
MSG_DONTROUTE,告诉协议不用查找路由表,目的主机在直接相连的网络上
它与套接口选项SO_DONTROUTE的差别是:SO_DONTROUTE影响这个套接口所有的输出操作(本次及以后);MSG_DONTROUTE只对本次发送起作用,下次发送如果没有设置这个标志,仍然会查找路由表。
-
说明:
-
函数send成功返回,意味着数据已经从用户进程复制到了协议发送缓冲区中,并不表明数据已经成功发送出去
-
协议发送数据可能失败,对于TCP协议,会重传数据,对于UDP数据就丢失了
使用UDP套接口,虽然send返回成功了,但可能发送过程中数据丢失了。
如果想确保数据一定发送给对方,那么要在应用程序中自己来保证,如要求对方收到数据时必须确认等,UDP协议并不保证数据的可靠传输。
-
如果传输层没有缓冲区空间容纳要求发送的数据,默认情况下,send将阻塞发送进程;
非阻塞模式下,面向连接的套接口根据本地和对端主机缓冲区大小,发送最大可能发送的数据。因此发送的数据长度可能介于1到要求的长度之间。
-
发送0长度的数据也是允许的,send返回值将是0,对于面向连接的套接口,会立即返回;而无连接套接口,会向对方发送0长度的数据报。
recv
函数recv从套接口接收数据,用于已经连接的套接口或者已绑定的套接口。
当套接口上没有数据时,默认情况下,recv阻塞调用进程,等待数据到达;非阻塞时,返回SOCKET_ERROR,错误码为WSAEWOULDBLOCK。程序可以用select或者WSAAsyncSelect来确定什么时候有数据到达
int WSAAPI recv(SOKCET s, char FAR*buf,int len,int flags);
-
调用成功时,返回接收的字节数。
返回值为0时,对于面向连接的socket,表示对方正常关闭了这个连接,对于无连接socket,是收到了对方发送的0长度的数据报;
发生错误时返回SOCKET_ERROR,调用WSAGetLastError()得到具体的错误码。
说明:
- 面向连接套接口,接收数据通常使用recv,但也可以使用recvfrom,除了最后两个参数被忽略外
- 系统将把尽可能多的数据复制到接收缓冲区中,但不能超过缓冲区的长度。协议中的数据大于recv提供的缓冲区长度时,系统将把len字节的数据复制到buf中,其他的数据仍然保留在队列缓冲区中。
- 如果在套接口上调用了SO_OOBINLINE配置为内连接收带外数据,并且协议接收队列中有未读的带外数据,则只返回带外数据。
- 对端正常关闭连接时,recv立即返回0,如果连接被重置,recv调用失败,返回SOCKET_ERROR,错误码为WSAECONNRESET。
- 无连接套接口,如UDP,只把输入队列中的第一个数据报复制到buf中,如果数据报的长度比缓冲区大,将只把数据报前面len字节的数据复制到buf中,多余的数据会丢失,recv产生错误码WSAEMSGSIZE。
- socket类型为UDP时,返回0,与使用TCP的socket是不同的,它不是对方关闭了连接,只表示接收到了一个0长度的数据报,因为UDP是无连接的,没有关闭连接操作。
- 标志flags影响recv函数的行为,可以是0或者一个或多个常量值的逻辑或组成,与recvfrom是相同的
shutdown
shutdown函数让套接口不能在某一个方向上传输数据
int WSAAPI shutdown(SOCKET s,int how);
-
成功返回0;失败返回SOCKET_ERROR,应用程序可以调用WSAGetLastError()得到具体的错误码。
-
how,标志,指明不再允许何种类型的操作。
-
how为SD_SEND,不允许之后的发送操作。TCP将向对方发送FIN,正常关闭发送方向的连接,但套接口仍然可以从对方接收数据,这是一个半关闭。当套接口缓冲区中还有数据时,TCP将把数据发送给对方,最后再发FIN,正常终止连接。
-
how为SD_RECEIVE,则关闭连接上的读操作,不再允许套接口上的读操作。MSDN说:SD_RECEIVE对底层协议没有影响。
TCP协议的窗口不会改变,如果套接口上仍有数据等待接收或随后还有数据到达,连接被重置,发送RESET。
UDP协议,输入的数据报被接受并且排队。两种协议都不会产生ICMP错误数据报。
RESET在RFC 793(Transmission Control Protocol)中是重置连接,整个连接被放弃了,这同时关闭了两个方向的数据传输。
- how为SD_BOTH时,发送和接收都被禁止。
-
说明:
-
TCP连接的半关闭
-
shutdown在网络编程中用得很少,套接口不再使用时,通常都直接调用closesocket。
-
shutdown主要用于面向连接的套接口,为了确保所有数据被发送和接收,应用程序应该在调用closesocket前用shutdown正常关闭连接。调用shutdown后,不能再重用这个套接口,尤其不允许再调用connect。
-
shutdown与closesocket的主要区别:
- 函数shutdown不关闭socket,也不释放套接口占用的资源;closesocket会释放资源。
- 每个套接口都有一个引用计数,closesocket把套接口的引用计数减1,减到0时关闭该套接口;shutdown不检查引用计数,无论其值是否为0都会执行,对于how为SD_SEND时总会发送FIN,正常终止发送方向上的连接。
- shutdown对套接口提供了更精确的控制,对TCP的全双工连接,可以关闭任何一个方向上的连接,而另一方向不受影响;closesocket总是关闭两个方向上的数据传输。
- 函数shutdown不阻塞调用进程,不受SO_LINGER选项的影响
getpeername
得到一个已连接套接口的对端地址
int WSAAPI getpeername(SOCKET s, struct sockaddr FAR*name,int FAR *namelen);
- 成功返回0,name包含了远端地址,namelen是name中地址的实际长度;失败返回SOCKET_ERROR,应用程序可以调用WSAGetLastError()得到具体的错误码。
这个函数几乎没用,因为在一个已经连接的socket上,对于客户端,一定调用了connect,而connect的最后两个参数就是远端的地址;对于服务器,如果对远端的地址感兴趣,可以通过accept来获得。
getsockname
得到套接口的本地地址
没有事先调用bind,或bind中的端口、地址是通配值时,连接成功后想获得系统为本地套接口分配的地址时,这个函数特别有用。
int WSAAPI getsockname(SOCKET s,struct sockaddr FAR *name,int FAR *namelen);
- 成功返回0,name包含了本地地址,namelen是name中地址的实际长度;失败返回SOCKET_ERROR,应用程序可以调用WSAGetLastError()得到具体的错误码。
说明:
-
函数getsockname用于已经绑定或者连接的套接口上,主要用于以下几种情况
- 应用程序在套接口上没有调用bind,而是直接调用了connect函数,系统会为应用程序选择合适的本地地址,并分配一个未用的端口。如果想知道系统分配给此套接口的本地地址和端口,可以在connect成功返回后,用getsockname得到。
- 用bind绑定时,端口号为0,要求系统为应用程序分配端口,getsockname返回由系统分配的端口号。
- 调用bind绑定的是一个通配地址INADDR_ANY,告诉系统可以使用任何在本机上可用的地址,特别在多穴(multi-homed)主机上,我们无法知道最后使用了哪个地址。
-
当套接口还没有连接时,调用getsockname可能无法得到本机地址的信息,只有套接口已经连接成功时,系统才会根据进入的连接选择一个匹配的本地地址。
-
服务器上,调用getsockname的套接口必须是已连接的套接口,而不能是侦听套接口。