1.6 面向连接的通信
本节讨论接受连接和建立连接所需要的 Winsock 函数。
首先讨论的是如何通过监听客户机连接来开发服务器,并探讨接受或拒绝一个连接的过程。随后讨论的是怎样通过初始化与服务
器的连接来开发客户机。最后讨论数据在面向连接会话中是如何传输的。
在IP 中,面向连接的通信是通过 TCP/IP 协议完成的。TCP 提供两个计算机间可靠无误的数据传输。应用程序使用 TCP 通信时,在源计算机和目标计算机之间,会建立起 个虚拟连接。
建立连接之后,计算机之间便能以双向字节流的方式进行数据交换。
1.6.1 服务器API函数
这里所说的服务器其实是个进程,它需要等待任意数量的客户机与之建立连接,以便为它们的请求提供服务。服务器必须在 -个已知的名称上监听连接。在 TCP/IP 中,这个名称就是本地接口的IP 地址,再加上一个端口编号。每种协议都有一套不同的寻址万案,所以务自的命名方法也不同。在Winsock 中,第·步是用 Socket WSASocker 将给定协议的套接绑定到它已知的名称上,这个过程是通过 bind API调用来完成的。下-步是将套接字置为监听模式,这·步用API函数 isten 来完成的。最后,若一台客户机试图建立连接,服务器必须通过 accept 或 WSAAccept 调用来接受连接。接下来的几个小节将讨论绑定、监听和接受客户机连接所需的每个 AP1 调用。图 1.1 展示了建立通信信道时,服务器和客户机必须执行的基本调用。
1.6.1.1 绑定
一旦为某种协议创建了套接字,就必须将套接绑定到一个已知地址上。bind 函数可将指定的套接字同一个已知地址绑定到–起。该函数的声明如下:
int bind( SOCKET s, const struct sockaddr FAR* name, int namelen );
其中,
第1个参数 s代表用来等待客户机连接的那个套接字,
第2个参数类型是 struct sockaddr它的作用很简单,就是一个普通的缓冲区。根据所使用的那个协议,必须实际地填充一个地址缓冲区并在调用 bind 时将其转换为一个 struct sockaddr。
Winsock 头文件将SOCKADDR 类型定义为 sttuctsockaddr。为简化起见,本章都将使用这个类型。
第 3 个参数代表要传递的、由协议决定的地址结构的长度。
举个例子来说,下列代码显示了在一个TCP 连接上,如何来做到这一点:
SOCKET s SOCKADDR_IN tcpaddr ; int port = 5150; s = socket(AF INET,SOCK STREAM,IPPROTO TCP) tcpaddr.sin_family = AF_INET; tcpaddr.sin_port = htons (port); tcpaddr.sin_addr.s_addr = htonl(INADDR_ANY); bind(s,(SOCKADDR *)&tcpaddr,sizeof(tcpaddr));
可以看到这个例子创建了一个流套接字。
随后,建立了 TCP/IP 地址结构,并打算在它上面接受客户机连接。
在这种情况下,通过特殊 P 地址INADDR ANY,套接字被绑定到默认的IP 接口,并占据5150端口。
可以指定系统中一个可用的显式IP 地址,不过INADDR_ANY
允许将套接字绑定到系统中所有可用的接口,以便将来传到任意接口上的客户机连接(必须在正确的端口上)都可以被监听套接字接受。
bind 调用正式将套接字同IP接口及端口关联到了一起。
一旦出错,bind就会返回SOCKET_ERROR。对bind而言,最常见的错误是WSAEADDRINUSE
如果使用的是TCP/IP,那么WSAEADDRINUSE
就表示另一个进程已经同本地P 接口及端口号绑定到了一起,或者那个P接口和端口号处于TIME WAIT 状态。
假如对一个已被绑定的套接字调用 bind,返回的将是WSAEFAULT
错误。
1.6.1.2 监听
接下来要做的,是将套接字置入监听模式。bind 函数的作用只是将套接字和指定的地址关联在一起。指示套接字等候连接传入的API函数则是 listen,其定义如下:
int listen( SOCKET s, int backlog
第 1 个参数同样是一个被绑定的套接字。backlog 参数指定了被搁置的连接的大队列长度。因为完全可能同时出现儿个服务器的连接请求,所以这个参数非常重要。例如,假定 backlog 参数为2.如果有3 个客户机同时发出请求,那么头两个会被放在个“并起”队列中,以便应用程序依次为它们提供服务。第3 个连接请求会失败,返网一个 WSAECONNREFUSED 错误。注意,”-旦服务器接受了一个连接,那个连接请求就会从队列中删去,以便别人可继续发出请求。backlog 参数其实本身就存在着限制,这个限制是由下层的协议提供程序决定的,如果这个参数出现非法值,那么系统会用与之最接近的一个合法值来取代。另外,对于如何找出实际的 backlog 值, 还不存在一种标准的方案。
listen 相关的错误是非常直观的。到目前为什么,取常见的错误是 WSAEINVAL
。该错误通常意味着,在调用 listen 之前没有调用 bind,另外,与 bind 调用相反,使用 listen 时可能接收到WSAEADDRINUSE
错误。这个错误通常是在进行 bind 调用时发生的。
1.6.1.3 接受连接
现在,我们已做好了接受客户机连的准备。这是通过 accept、WSAAccept 或 AcceptEx 两数来完成的(AcceptEx 是accpt 的扩展版本,第6章中对它作广详描述)。
Aceept 的原型如下:
SOCKET accept( SOCKET s, struct sockaddr FAR* addr, int FAR* addrler );
其中,参数 s 是·个被绑定的接字,它处于监听模式。
第 2 个参数应该是一个有效的SOCKADDR_IN
结构的地址,而 addrlen 应该是 SOCKADDR_IN
结构的长度。对于属于另一种协议的套接字,应当用与那种协议对应的 SOCKADDR
结构来替换 SOCKADDR_IN
。
通过对 accpet 函数的调用,可以为被搁置的连接队列中的第 1 个连接请求提供服务accpet 函数返回后,addr 结构中会包含发出连接请求的那个客户机的IPv4地址信息,而addrlen参数则指出addr 结构的长度。
此外, accpet会返回一个新的套接字描述符,它对应于已经被接受的那个客户机连接、该客户机后续的所有操作都应该使用这个新的套接字。全于原来那个监听套接子,它仍然用于接受其他客户机连接,而且仍处于监听模式。
如果出错,INVALID_SOCKET 将被返回。在监听套接字为异步或非阻塞模式,并且没有连接被接受时,而常见的错误是 WSAEWOULDBLOCK
,阻塞、非阻塞以及其它套接字模式将在第 5章中作介绍。
Winsock2引入了WSAAccept 函数,根据条件雨数的返回值,该函数可有条件地接受··个连接第 10章将详细地讲述WSAAccept 函数。
到此为止,已经介绍过了创建简单的 Winsock TCP/IP 服务器应用程序所需的全部元素。下面的程序片断展示了如何编写一个能够接受 TCP/IP 连接的服务器。程序中没有对调用实施任何错误检查这样可以使代码显得清晰易读。本应用程序的完整版本可在补充材料中的 TCPSERVER 文件中找到
finclude <winsock2.h> void main(vold) { WSADATA wsaData; SOCKET ListeningSocket; SOCKET NewConnection; SOCKADDR_IN ServerAddr; SOCKADDR_IN ClientAddr; int Port = 5150; //初始化 qinsock 版本 2.2 WSAStartup (MAKEWORD(2,2),weaDatal; //创建-个新的套接字来监听客户机连接 ListeningSocket = socket(AF INET,SOCK STREAM,IPPROTO TCP);//建立一个S0CKADDR IN 构,这个结构将告知 bind 我们想要在 5150 端口监听所有接口上 //的连接 //请留意这里是如何将端口变量从机字节顺序转换为网络字节顺序的 ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons[Port); ServerAddr.sin_addr.s_addr = htonl(INADDR ANY): //使用 bind 将这个地址信息和套接字关联起来 bind(ListeningSocket,(SOCKADDR)&ServerAddr,sizeof(ServerAddr)); //监听客户机连接。这里使用 5 个 backlog,许多应用程序一般都使用这个数量 listen(ListeningSocket,5); //连接到达时,接受一个新连接 NewConnection = accept(ListeningSocket,(SOCKADDR *)&ClientAddr, &ClientAddrlen)); //此刻在这些套接字上可以做两件事。是在 ListeningSocket 上再次调用 accept//等候更多的连接到来。二是开始在 newConnection 上收发数据。本章稍后将讲述怎 //样进行数据收发 //在 NewConnection 套接字上完成数据收发,以及在 ListeningSocket 套接字上 //完成接受新连接府,应该用 closesocket API 关闭这些套接宁 //本章稍后将进述套接字的关闭 closcsocket(Newconnection); closesocket(ListeningSocket); //应用程序完成对连接的处理后,谢用 WSACleanup WSACleanup();
在了解到怎样创建一个能够接收客户机连接的服务器之后,下面接着讲述如何创建客户机。
1.6.2 客户端API函数
客户机的创建要简单得多,建立成功连接所需的步骤也要少得多。创建客户机只需 3 步操作:
- 创建一个套接字。
- 建立·个 SOCKADDR 地址结构,结构名称为准备连接到的服务器名(以下层协议为准)。对于 TCP/IP,这是客户机应用程序所监听的服务器的 IP 地址和端口号。
- 用 connect 或 WSAConnect 给化客户机与服务器的连接。
由于已经知道如何建立套接字和建立SOCKADDR 结构,所以现在只有一步未做,那就是建立一个连接。
TCP 状态
作为一名 Winsock 程序员,通常没必要了解实际的 TCP 状态,但了解了TCP 状态,就能更好地理解 Winsock API调用如何对下层协议中的变化产生影响。
此外,许多程序员在关闭套接字时,会碰到一个常见问题,那就是围绕套接字关闭时的 TCP 状态,这是我们目前最感兴趣的问题对每个套接字来说,它的初始状态都是 CLOSED
,若客户机初始化了一个连接,就会向服务器发送一个 SYN
包,同时将客户机套接字状态置为 SYN SENT
。
服务器收到SYN
包后,会发出一个SYN-ACK
包,客户机需要用一个ACK
包对它做出响应此时,客户机的套接字将处于ESTABLISHED
状态,如果服务器一直不发送 SYN-ACK
包,客户机就会超时,并返回CLOSED
状态若服务器的套接字同本地接口及端口绑定起来,并在它上面进行监听,那么套接宇的状态便是LISTEN
,客户机试图与服务器连接时,服务器就会收到一个 SYN
包,并用一个SYN-ACK
包做出回应,服务器套接字的状态就变成SYN RCVD
,最后,客户机发出一个ACK 包,它将使服务器套接字的状态变成ESTABLISHED
.
一旦应用程序处于ESTABLISHED
状态,就可以通过两种方法来关闭它。
如果由应用程序来关闭便叫作“主动套接字关闭
”, 否则,套接字的关闭便是被动的。
图 1.2 对两种关闭方法进行了解释。
如主动关闭,应用程序便会发出一个 FIN 包。应用程序调用 closesocket
或 shutdown
时(把SD_SEND
当作第 2个参数),会向通信对方发出一个 FIN
包,而且套接字的状态将变成FIN_WAIT1
.正常情况下,通信对方会用一个 ACK
包作为回应,套接宇的状态随之变成FIN WAIT 2
,如通信对方也关闭了连接,它会发出一个 FIN
包,我们的机器则会以一个ACK
包作为回应,并将套接字的状态置为TIME WAIT
.
TIME WAIT状态
也叫作 2MSL 等待状态
。其中,MSL 代表“分段最长生存时间”(MaximumSegment Lifetime)
,表示一个数据包在被丢弃之前,可在网络上存在多长时间,每个1 包都含有一个TTL(time-to-live,生存时间)字段,若它递减到0、包便会被丢弃。 一个包经过网络上的每个路由器时TTL值都会减1,然后继续传递,一旦应用程序进入 TIME WAIT
状态,那么就会一直持续两倍 MSI的时间。这样,如果最后一个ACK
包丢失,TCP 就可以重新发送它,也就是说,FIN
会被重新传送出去。两倍于MSL 时间的等待状态结束之后,套接字使进入 CLOSED
状态。
采取主动关闭措施时,沿两条路径可以进入 TIME WAIT
状态。在以前的讨论中,只有一方发出一个FIN
,并接收一个ACK
回应。然而,另一方仍然可以自由地发送数据,直到它也被关闭为止。
因此,需要另外两条路径发挥作用,在一条路径中(即同步关闭),一台计算机与之连接的通信对方会同时要求关闭: 计算机向对方送出一个FIN
款据包,并从它那里接收一个 FIN
数据包。随后,计算机会发出一个ACK
数据包,对对方的 FIN
包作出回应,并将自己的套接字置为 CLOSING
状态。计算机从对方那里接收到最后一个ACK
包之后,它的套接字状态会变成TIME WAIT
,主动关闭时,另一条路径其实就是同步关闭的变体:
- 套接字从
FIN WAIT
状态直接变成TIME WAIT
状态。
若应用程序发出一个 FIN
数据包,但马上又从对方那里接收到一个 FIN-ACK
包时,这种情况就会发生。在这种情况下,对方会确认是否收到了应用程序的 FIN
包,并送出自己的FIN
包对于这个包,应用程序会以一个 ACK
包做出回应
TIME WAIT 状态
的主要作用是,在TCP 连接处于2MSL等待状态的时候,定义那个连接的一对套接宇不可以被重新使用。
这对套接宇由本地 IP 端口以及远程IP 端口组成。某些TCP 实施方案不许重新使用处于 TIME WAIT 状态
下的套接字对中的任何端口号,在微软实现的 TCP 中,不会存在这个问题,然而,若试图通过一对已处于TIME WAIT 状态的套接字建立连接,连接的建立就会失败并返回 WSAEADDRINUSE
错误。要解决这一问题(除了等待使用本地端口的套接字对脱离TIML WAIT 状态
之外),一个办法是使用套接字选项 SO REFUSEADDR,第7章将对这个选项进行详细讨论.
在被动关闭情况下,应用程序会从对方那里接收到一个 FIN
包,并用一个 ACK
包做出回应。此时,应用程序的套接宇会变成 CLOSE WAIT
状态。由于对方已关闭自己的终端,所以它不能再发送数据了,但应用程序却不同,它能一直发送数据,直到它关闭了自己的连接终端为止,要想连接终端应用程序需要发出自己的FIN
,今应用程序的套接宇状态变成LAST ACK
,应用程序从对方接收到一个 ACK
包后,它的套接字就会回到 CLOSED
状态。要想了解TCP/IP 协议的有关详情,请访问 http://www.rfc-editor.org,查阅RFC793文件。
connect 函数
连接套接字是通过调用 connect、WSAConnect 或 ConnectEx 丽数来完成的。先来看看 connect 函数的Winsock1版本,其定义如下
int connect ( SOCKET s, const struct sockaddr FAR* name, int namelen, );
这个函数的参数含义是相当明显的:
- 参数s是即将在其上面建立连接的那个有效 TCP 套接字
- name 参数是TCP 的套接字地址结构 SOCKADDR_IN,表示要连接到的服务器
- namelen 则是name参数的长度。
如果想连接的计算机没有进程用于监听给定端口,connect 调用就会失败,并产生WSAECONNREFUSED
。
另一个错误可能是WSAETIMEDOUT
,这种情况,般发生在试图连接的计算机不可用时(也可能因为到主机之间的路由上出现硬件故障,或主机目前没有联网)。
下面的程序片断展示了如何编写一个能够连接到前述服务器应用程序的简单客户机。程序中没有对调用实施任何错误检查,以使代码显得清晰易读。本应用程序的完整版本可在补充材料中名为TCPCLIENT 的文件中找到。
#inelude <winsock2.h> void main(void){ WSADATA wsaData; SOCKET s; SOCKADDR_IN ServerAddr; int Port = 5150; //初始化 winsock 2.2 版本 WSAStartup(MAKEWORD(2,2),wsaData); //创建·个新的套接宁来建立客户机连接 s = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); //建立一个 SOCKADDR IN 结构,用它来连接到在5150 端的监听服务器//作为示范,这里假设我们的服务器 IP 地址是 1361493.29//显然,应该提示用户输入 IP 地址,并将用户数据填入这个字段 ServerAddr.sin_famlly = AF_INET; ServerAddr.sin_port = htons (Port); ServerAddr.sin_addr.s_addr = inet_addr("136.149,3.29); //用会接子 s 创建个到服务器的连接 connect(s,(SOCKADDR *)&ServerAddr,sizeof(ServerAddr)); //此时可以在套接 s 上收发数据了。本章稍后将叙述怎//样进行数据收发 //在套接字 s 上结束数据收发后,应该用 closesocket API 来关闭它 //本章稍后将叙述套接字的关闭 closesocket(s); //应用程序完成对连接的处理后,调用 WSACleanup WSACleanup(); }
既然已经能够为面向连接的服务器和客户机建立通信了,接下来便开始处理数据传输