Windows网络编程:一文深入理解Winsock

Winsock是一种标准API,主要用于网络中的数据通信,允许两个或者多个应用程序(或进程)在同一机器上或通过网络相互通信。使用Winsock API,应用程序可以通过TCP/IP或UDP协议建立网络通信。

Winsock API包括Winsock1和Winsock2版本,Winsock2版本的函数通过前缀"WSA-"标识。比如,建立套接字的Winsock1函数称为socket,而在Winsock2中则称为WSASocket。注意,该命名规则有几个函数例外,即WSAStartup、WSACleanup、WSARecvEx和WSAGetLastError都属于Winsock1.1版本规范的函数。

1.Winsock头文件

每个Winsock应用都必须加载合适的Winsock DLL版本,否则会返回SOCKET_ERROR,错误信息为WSANOTINITIALISED。

Winsock版本及头文件、库文件
使用版本 包含头文件 链接库
Winsock1 WINSOCK.H  WSOCK32.LIB
Winsock2 WINSOCK2.H WS2_32.LIB
ANY MSWSOCK.H MSWSOCK.DLL

2.Winsock使用详解

2.1.初始化WSAStartup和释放WSACleanup

2.1.1.初始化

调用WSAStartup函数加载Winsock库

int PASCAL FAR WSAStartup
(
   _In_ WORD wVersionRequired,
   _Out_ LPWSADATA lpWSAData
 );
  • wVersionRequired:指定加载的Winsock的版本,高位字节-次版本,低位字节-主版本。可用MAKEWORD(x,y)获得,其中x-高位字节,y-低位字节。
  • lpWSAData:指针,指向一个WSADATA结构体,该结构体定义及含义如下:
	typedef struct WSAData {
		WORD                    wVersion;      // 设定为将要使用的Winsock版本
		WORD                    wHighVersion;  // 现有的Winsock库的最高版本
#ifdef _WIN64
		unsigned short          iMaxSockets;                            // 可以同时打开的最大套接字数量,一般不使用
		unsigned short          iMaxUdpDg;                              // 数据报的最大长度,一般不使用
		char FAR *              lpVendorInfo;                           // 保留字段,存储特定供应商信息
		char                    szDescription[WSADESCRIPTION_LEN + 1];  // 无实际作用
		char                    szSystemStatus[WSASYS_STATUS_LEN + 1];  // 无实际作用
#else
		char                    szDescription[WSADESCRIPTION_LEN + 1];
		char                    szSystemStatus[WSASYS_STATUS_LEN + 1];
		unsigned short          iMaxSockets;
		unsigned short          iMaxUdpDg;
		char FAR *              lpVendorInfo;
#endif
	} WSADATA;

2.1.2.释放

调用WSACleanup释放所有由Winsock分配的资源。每次调用WSAStartup后都应该调用WSACleanup。

int PASCAL FAR WSACleanup(void);

2.2.错误检查

调用WSAGetLastError(void)来获得发生所错的整数代码。与此对应的是,可以调用WSASetLastError来手动设置WSAGetLastError获取的错误代码。

2.3.IPv4寻址

使用TCP或UDP协议时需要用到IP地址和端口号,IP地址由一个32位的数值表示。Winsock中通过SOCKADDR_IN结构体指定IP地址和端口号

struct sockaddr_in {
		short   sin_family;       // 设定为AF_INET, 表示正使用IP地址族
		u_short sin_port;         // 端口号
		struct  in_addr sin_addr; // 把IP地址作为一个4字节变量存储
		char    sin_zero[8];
	};

2.3.1.字节排序

  • 主机字节:即little-endian,计算机中把IP和端口号指定为多字节数时,按照主机字节顺序表示;
  • 网络字节:即big-endian,网络上指定IP和端口号时,使用网络字节顺序表示。
字节顺序转换函数
  API
主机字节顺序 --> 网络字节顺序

u_long htonl ( _In_ u_long hostlong);

u_short htons (_In_ u_short hostshort);

网络字节顺序 --> 主机字节顺序

u_long ntohl (_In_ u_long netlong);

u_short ntohs (_In_ u_short netshort);

 下列代码示例如何进行IPv4寻址:

#define PORT 8001  
#define SERVER_IPADRESS "127.0.0.1"     

sockaddr_in server_addr;  
server_addr.sin_family = AF_INET;  
server_addr.sin_addr.S_un.S_addr = inet_addr(SERVER_IPADRESS);  
server_addr.sin_port = htons(PORT);

2.4.创建套接字

套接字是传输提供程序的句柄。Winsock中有两个函数可以用于创建套接字:socket和WSASocket。

	SOCKET PASCAL FAR socket(
		_In_ int af,        // 协议地址族,如果使用IPv4,该值设为AF_INET
		_In_ int type,      // 套接字类型,TCP/IP:SOCK_STREAM, UDP/IP:SOCK_DGRAM
		_In_ int protocol); // TCP:IPPROTO_TCP,  UDP:IPPROTO_UDP

2.5.面向连接的通信

面向连接的通信即TCP/IP协议(计算机网络——运输层),通过TCP建立可靠传输,TCP的特点之一就是传输前建立连接。因此,在使用TCP传输时,需要在服务器和客户端之间建立一个虚拟连接。

2.5.1.服务器端

服务器端需要在指定地址的指定端口上监听连接(IP地址:端口)。TCP/IP中,服务器端的操作步骤如下:

  • (1)通过Socket或WSASocket初始化套接字,并绑定到指定IP和端口上(bind);
  • (2)设置套接字为监听模式(listen);
  • (3)若客户端试图建立连接,接受连接(accept WSAAccept

(1)bind

int PASCAL FAR bind(
		SOCKET s,                        // 套接字                                
		const struct sockaddr FAR *addr, // 缓冲区,根据具体使用的协议填充
		int namelen);                    // 要传递的(由协议决定的)地址结构的长度

其中,sockaddr是一个结构体 。下述代码示例bind的使用:

#define PORT 8001  
#define SERVER_IPADRESSADRESS "127.0.0.1"  

	// 1. 声明并初始化一个服务端(本地)的地址结构  
	sockaddr_in server_addr;
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.S_un.S_addr = INADDR_ANY;
	server_addr.sin_port = htons(PORT);

	// 2. 创建socket  
	SOCKET m_Socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (SOCKET_ERROR == m_Socket)
	{
		printf("Create Socket Error!");
		exit(1);
	}

	// 3. 绑定socket和服务端(本地)地址  
	if (SOCKET_ERROR == bind(m_Socket, (LPSOCKADDR)&server_addr, sizeof(server_addr)))
	{
		printf("Server Bind Failed: %d", WSAGetLastError());
		exit(1);
	}

(2)listen

int PASCAL FAR listen(
		_In_ SOCKET s,         // 套接字
		_In_ int backlog);

其中第二个参数backlog指定被搁置的连接的最大队列长度。很可能的情况是,同时出现几个服务器的请求。如果backlog设置为2,当有3个客户端进程同时发出连接请求,那么前两个连接会被放入队列中排队等待连接,而第3个里连接请求会失败,并返回一个WSACONNREFUSED错误。当服务端接受某一个连接,那个连接就会从队列中被删除。 

(3)accept 

SOCKET PASCAL FAR accept(
		_In_ SOCKET s,
		_Out_writes_bytes_opt_(*addrlen) struct sockaddr FAR *addr,
		_Inout_opt_ int FAR *addrlen);

accept返回后,addr里保存的是请求连接的客户端进程的IPv4地址信息,addrlen是该信息的长度。

accept会返回一个新的套接字描述符。原来那个监听的套接字仍旧可用于监听接受别的客户端连接。 

2.5.2.客户端

客户端的步骤包括:

  • (1)创建一个套接字;
  • (2)建立一个SOCKADDR地址结构,结构名称为准备连接到的服务器名(IP:端口号);
  • (3)用connect / WSAConnect初始化客户端与服务器的连接。
int PASCAL FAR connect(
		SOCKET s,                       // 套接字
		const struct sockaddr FAR *name,// 套接字地址结构,表示要连接到的服务器
		int namelen);                   // name参数的长度

如果想连接的计算机没有进程用于监听给定端口,connect调用会失败。

2.5.3.数据传输

网络编程的主要目的是收发数据。在上述建立的套接字的基础上,可以用send / WSASend 和 recv / WSARecv来在已建立连接的套接字上收发数据。所有关系到收发数据的缓冲区都属于简单的char类型(即面向字节的数据)

所有收发函数返回的错误码都是SOCKET_ERROR,通过WSAGetLastError可以获取收发函数返回的错误码。常见的错误是WSACONNABORTED和WSACONNRESET,表示要么连接超时被关闭,要么由于通信方正在关闭连接。另一个错误WSAWOULDBLOCK表示套接字处于非阻塞模式或异步状态。

(1)send / WSASend

int PASCAL FAR send(
		_In_ SOCKET s,                             // 已建立连接的套接字
		_In_reads_bytes_(len) const char FAR * buf,// 包含即将发送数据的缓冲区
		_In_ int len,                              // 指定缓冲区内的字符数
		_In_ int flags);      // 0
                                      // MSG_DONTROUTE:要求传输层不要将它发出的数据包路由出去
                                      // MSG_OOB:数据应该进行带外发送(紧急数据)

int WSAAPI WSASend(
		_In_ SOCKET s,                                // 已建立连接的套接字
		_In_reads_(dwBufferCount) LPWSABUF lpBuffers, // 指向一个或多个WSABUF结构的指针(独立的结构或者结构数组)
		_In_ DWORD dwBufferCount,                     // 指明传递的WSABUF结构的数量
		_Out_opt_ LPDWORD lpNumberOfBytesSent,        // 已发送的总字节数
		_In_ DWORD dwFlags,                           // 同send中的flags参数
		_Inout_opt_ LPWSAOVERLAPPED lpOverlapped,     // 用于异步I/O
		_In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 用于异步I/O
		);

正常情况下,send函数发挥发送的字节数;若发生错误,这返回SOCKET_ERROR,具体错误可调用WSAGetLastError获得。WSASend执行成功时返回0,否则返回SOCKET_ERROR.

(2)recv / WSARecv

int PASCAL FAR recv(
		_In_ SOCKET s,              // 准备接收数据的套接字
		_Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf, // 用于接收数据的缓冲区
		_In_ int len,               // 准备接收数据的缓冲区的长度
		_In_ int flags);            // 0;MSG_PEEK:将可用的数据复制到所提供的接收缓冲区里;MSG_OOB:

int WSAAPI WSARecv(
		_In_ SOCKET s,                           // 准备接收数据的套接字
		_In_reads_(dwBufferCount) __out_data_source(NETWORK) LPWSABUF lpBuffers,// 指向由多个WSABUF结构组成的数组
		_In_ DWORD dwBufferCount,                // 数组中WSABUF结构的数量
		_Out_opt_ LPDWORD lpNumberOfBytesRecvd,  // 收到的字节数
		_Inout_ LPDWORD lpFlags,                 // MSG_PEEK ; MSG_OOB ; MSG_PARTIAL-只随面向消息的协议一起使用
		_Inout_opt_ LPWSAOVERLAPPED lpOverlapped,
		_In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
		);

正常情况下,recv函数返回接收的字节数,WSARecv函数返回0.

2.5.4.使用面向连接的协议(流协议)时的数据传输

在流协议中,发送者和接收者可以将数据分解为小块数据或将数据合并为大块数据。流套接字并不能保证要求进行读取或者写入的数据量。比如在send函数中第三个参数可指定待发送的缓冲区的字节数,假设指定len=2048,并声明了一个长度为2048的sendbuf。send函数可能返回小于2048。这是因为系统为每个流套接字分配了足够空间的缓冲区空间,内部缓冲区空间会将数据一直保留到可以将它们发送到线上为止。所以,传输大量数据时可以迅速将内部缓冲区填满,而少量数据则不然。同时,在TCP/IP中接收端还可以调整窗口大小(指示可以接收多少数据)。流套接字上接收数据时该原则同样适用。

要保证将所有字节发送出去,可参照下述代码:

char sendBuf[2048];// 假设发送缓冲区已经被数据填满
	int nBytes = 2048;
	int bytesLeft = nBytes;
	int bytesSent = 0;
	int ret = 0;

	while (bytesLeft > 0){
		ret = send(s, &sendBuf[bytesSent], bytesLeft, 0);
		if (SOCKET_ERROR == ret){
			// error
		}
		bytesLeft -= ret;
		bytesSent += ret;
	}

要保证读取完所有数据,可参考下述代码:

char recvBuf[2048];// 假设发送缓冲区已经被数据填满
	int nBytes = 2048;
	int bytesLeft = nBytes;
	int bytesSent = 0;
	int ret = 0;

	while (bytesLeft > 0){
		ret = recv(s, &recvBuf[bytesSent], bytesLeft, 0);
		if (SOCKET_ERROR == ret){
			// error
		}
		bytesLeft -= ret;
		bytesSent += ret;
	}

2.5.5.中断连接

可先调用shutdown函数从容地终止连接,再调用closesocket释放套接字相关的资源。如果直接执行closesocket,可能会导致数据丢失,所以在调用closesocket之前,利用shutdown函数从容地终止连接。

int PASCAL FAR shutdown(
		_In_ SOCKET s,
		_In_ int how);

how可以是以下值:

  • SD_RECEIVE:表示不允许再调用接收函数
  • SD_SEND:不允许再调用发送函数
  • SD_BOTH: 取消连接两端的收发操作

2.5.6.TCP通信代码

Winsock实现TCP通信代码实例见:Windows网络编程:Winsock实现客户端与服务器文件传输(TCP/IP)

2.6.无连接的通信

无连接的通信是通过UDP/IP协议完成。

2.6.1.接收端

操作步骤上,先用socket / WSASocket创建套接字;再把该套接字和准备接收数据的接口绑定在一起(通过bind函数);然后只需等待收发数据。(不必调用listen和connect)。调用recvfrom函数:

int PASCAL FAR recvfrom(
		SOCKET s,                  // 准备接收数据的套接字
		char FAR * buf,            // 用于接收数据的缓冲区
		int len,                   // 准备接收数据的缓冲区的长度
		int flags,                 // 0;MSG_PEEK:将可用的数据复制到所提供的接收缓冲区里;
		struct sockaddr FAR * from,// SOCKADDR结构
		int FAR * fromlen);        // 带有指向地址结构长度的fromlen

recvfrom函数返回数据时, SOCKADDR结构内填入了发送数据的那个工作站(计算机)的地址信息。

也可调用WSARecvFrom函数

int	WSAAPI WSARecvFrom(
		SOCKET s,
		LPWSABUF lpBuffers,
		DWORD dwBufferCount,
		LPDWORD lpNumberOfBytesRecvd,
		LPDWORD lpFlags,
		struct sockaddr FAR * lpFrom,
		LPINT lpFromlen,
		LPWSAOVERLAPPED lpOverlapped,
		LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
		);

2.6.2.发送端

在无连接的套接字上发送数据,可先创建一个套接字,然后调用sendto / WSASendTo函数发送数据。

int PASCAL FAR sendto(
		SOCKET s,
		const char FAR * buf,
		int len,
		int flags,
		const struct sockaddr FAR *to,
		int tolen);

int	WSAAPI WSASendTo(
		SOCKET s,
		_LPWSABUF lpBuffers,
		DWORD dwBufferCount,
		LPDWORD lpNumberOfBytesSent,
		DWORD dwFlags,
		const struct sockaddr FAR * lpTo,
		int iTolen,
		LPWSAOVERLAPPED lpOverlapped,
		LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
		);

参数的含义与recvfrom / WSARecvFrom相同,不再赘述。

2.6.3.释放套接字资源

无连接协议没有建立连接,所以直接调用closesocket即可。

2.6.4.UDP通信代码

Winsock实现UDP通信代码见: Windows网络编程——Winsock实现UDP通信

你可能感兴趣的:(计算机网络,Windows开发)