天天看点

《Windows网络与通信程序设计(第3版)》——2.3 Winsock编程详解

本节书摘来自异步社区《windows网络与通信程序设计(第3版)》一书中的第2章,第2.3节,作者: 陈香凝 , 王烨阳 , 陈婷婷 , 张铮 更多章节内容可以访问云栖社区“异步社区”公众号查看。

使用tcp创建网络应用程序稍微复杂一些,因为tcp是面向连接的协议,需要通信双方首先建立一个连接。本节先以建立简单的tcp客户端和服务器端应用程序为例,详细说明winsock的编程流程,然后再介绍较为简单的udp编程。

2.3.1 winsock编程流程

使用winsock编程的一般步骤是比较固定的,可以结合后面的例子程序来理解它们。

1.套接字的创建和关闭

使用套接字之前,必须调用socket函数创建一个套接字对象,此函数调用成功将返回套接字句柄。

type参数用来指定套接字的类型。套接字有流套接字、数据报套接字和原始套接字等,下面是常见的几种套接字类型定义。

sock_stream:流套接字,使用tcp提供有连接的可靠的传输

sock_dgram:数据报套接字,使用udp提供无连接的不可靠的传输

sock_raw:原始套接字,winsock接口并不使用某种特定的协议去封装它,而是由程序自行处理数据报以及协议首部。

当type参数指定为sock_stream和sock_dgram时,系统已经明确使用tcp和udp来工作,所以protocol参数可以指定为0。

函数执行失败返回invalid_socket(即-1),可以通过调用wsagetlasterror取得错误代码。

也可以使用winsock2的新函数wsasocket来创建套接字,与socket相比,它提供了更多的参数,如可以自己选择下层服务提供者、设置重叠标志等,后面再具体讨论它。

当不使用socket创建的套接字时,应该调用closesocket函数将它关闭。如果没有错误发生,函数返回0,否则返回socket_error。函数用法如下。

2.绑定套接字到指定的ip地址和端口号

为套接字关联本地地址的函数是bind,用法如下。

bind函数用在没有建立连接的套接字上,它的作用是绑定面向连接的或者无连接的套接字。套接字被socket函数创建以后,存在于指定的地址家族里,但它是未命名的。bind函数通过安排一个本地名称到未命名的socket而建立此socket的本地关联。本地名称包含3部分:主机地址、协议号(分别为udp或tcp)和端口号。

本节的tcpserver程序使用以下代码绑定套接字s到本地地址。

sockaddr_in结构中的sin_familly字段用来指定地址家族,该字段和socket函数中的af参数的含义相同,所以唯一可以使用的值就是af_inet。sin_port字段和sin_addr字段分别指定套接字需要绑定的端口号和ip地址。放入这两个字段的数据的字节顺序必须是网络字节顺序。因为网络字节顺序和intel cpu的字节顺序刚好相反,所以必须首先使用htons函数进行转换。

如果应用程序不关心所使用的地址,可以指定internet地址为inaddr_any,指定端口号为0。如果internet地址等于inaddr_any,系统会自动使用当前主机配置的所有ip地址,简化了程序设计;如果端口号等于0,程序执行时系统会为这个应用程序分配唯一的端口号,其值在1024~5000之间。应用程序可以在bind之后使用getsockname来知道为它分配的地址。但是要注意,直到套接字连接上之后getsockname才可能填写internet地址,因为对一个主机来说可能有多个地址是可用的。

tcp客户端程序也可以在不显式绑定地址和端口号的情况下发送数据或者连接。在这种情况下,系统也会默认地为套接字绑定一个本地端口(1024~5000之间)。

3.设置套接字进入监听状态

listen函数设置套接字进入监听状态。

为了接受连接,首先使用socket函数创建套接字,然后使用bind函数将它绑定到本地地址,再用listen函数为到达的连接指定backlog,最后使用accept接受请求的连接。

listen仅应用在支持连接的套接字上,如sock_stream类型的套接字。函数执行成功后,套接字s进入了被动模式,到来的连接会被通知要排队等候接受处理。

在同一时间处理多个连接请求的服务器通常使用listen函数,如果一个连接请求到达,并且排队已满,客户端将接收到wsaeconnrefused错误。

4.接受连接请求

accept函数用于接受到来的连接。

该函数在 s 上取出未处理连接中的第一个连接,然后为这个连接创建新的套接字,返回它的句柄。新创建的套接字是处理实际连接的套接字,它与s有相同的属性。

程序默认工作在阻塞模式下,这种方式下如果没有未处理的连接存在,accept函数会一直等待下去,直到有新的连接发生才返回。

addrlen参数用于指定addr所指空间的大小,也用于返回地址的实际长度。如果addr或者addrlen是null,则没有关于远程地址的信息返回。

客户端程序在创建套接字之后,要使用connect函数请求与服务器连接,函数原型如下。

第一个参数s是此连接使用的客户端套接字,另两个参数name和namelen用来寻址远程套接字(正在监听的服务器套接字)。

5.收发数据

对流套接字来说,一般使用send和recv函数来收发数据。

send函数在一个连接的套接字上发送缓冲区内的数据,返回发送数据的实际字节数。recv函数从对方接收数据,并将其存储到指定的缓冲区。flags参数在这两个函数中通常设为0。

在阻塞模式下,send将会阻塞线程的执行直到所有的数据发送完毕(或者发生错误),而recv函数将返回尽可能多的当前可用信息,直到达到缓冲区指定的大小。

2.3.2 典型过程图

tcp服务器程序和客户程序的创建过程如图2.2所示。服务器端创建监听套接字,并为它关联一个本地地址(指定ip地址和端口号),然后进入监听状态准备接受客户的连接请求。为了接受客户端的连接请求,服务器端必须调用accept函数。

客户端创建套接字后即可调用connect函数去试图连接服务器监听套接字。当服务器端的accept函数返回后,connect函数也返回。此时客户端使用socket函数创建了套接字,服务器端使用accept函数创建了套接字,双方就可以通信了。

2.3.3 tcp服务器和客户端程序举例

下面是最简单的tcp服务器程序和tcp客户端程序的例子。这两个程序都是控制台界面的win32应用程序,分别在配套光盘的tcpserver和tcpclient工程下。

运行服务器程序tcpserver,如果没有错误发生,将在本地机器上的4567端口上等待客户端的连接。如果没有连接请求,服务器会一直处于休眠状态。

运行服务器之后,再运行客户端程序tcpclient,其最终效果如图2.3所示。客户端连接到了服务器,双方套接字可以通信了。

《Windows网络与通信程序设计(第3版)》——2.3 Winsock编程详解

下面是tcpserver程序源代码。

下面是tcpclient程序源代码。

2.3.4 udp编程

tcp由于可靠、稳定的特点而被用在大部分场合,但它对系统资源要求比较高。udp是一个简单的面向数据报的传输层协议,又叫用户数据报协议。它提供了无连接的、不可靠的数据传输服务。无连接是指它不像tcp那样在通信前先与对方建立连接以确定对方的状态。不可靠是指它直接按照指定ip地址和端口号将数据包发出去,如果对方不在线的话数据可能丢失。

1.udp编程流程

(1)服务器端程序设计流程如下。

① 创建套接字(socket)。

② 绑定ip地址和端口(bind)。

③ 收发数据(sendto/recvfrom)。

④ 关闭连接(closesocket)。

(2)客户端程序设计流程如下。

② 收发数据(sendto/recvfrom)。

③ 关闭连接(closesocket)。

udp用于发送和接收数据的函数是sendto和recvfrom,它们的用法如下。

同样,udp接收数据时也需要知道通信对端的地址信息。

这个函数比recv函数多出最后两个参数,from 参数是指向sockaddr_in结构的指针,函数在这里返回数据发送方的地址,fromlen参数用于返回前面的sockaddr_in结构的长度。

2.udp编程举例

下面是一个最简单的udp服务器程序udpserver。它运行之后,进入无限循环,监听4567端口到来的udp封包,如果发现就将用户数据以字符串形式打印出来。相关代码如下。

客户端程序更简单,创建套接字之后,调用sendto即可向指定地址发送数据。本例中相关代码如下。

值得注意的是,创建套接字之后,如果首先调用的是sendto函数,则可以不调用bind函数显式地绑定本地地址,系统会自动为程序绑定,因此今后即便是调用recvfrom也不会失败(因为套接字已经绑定了)。但是,如果创建套接字之后,直接调用recvfrom就会失败,因为套接字还没有绑定。

继续阅读