Windows Sockets 2 笔记

一、Winsock简介

MSDN原文链接。
Socket技术简介视频(看前半部分即可)。
 Windows Sockets 2简写为Winsock,它的作用是使程序员能够创建高级 Internet(互联网) 、Intranet(内联网) 和其他种类支持网络的应用程序。
 Winsock使得程序能够跨网络传输应用程序数据,并且独立于所使用的网络协议。
 借助Winsock,程序员可以访问高级Microsoft Windows网络功能,例如多播和服务质量等。
 从前的Winsock编程以TCP/IP协议为主,但使用TCP/IP的编程写法却不适用于其他类型的协议,因此Winsock API会根据需要添加函数来处理其他协议类型。
 Windows Socket 2 为C/C++程序员设计,它可以在所有的Windows平台上使用,如果平台存在某些实现或功能限制,则会在文档中明确指明。

二、Windows中Winsock对网络协议支持的情况

MSDN原文链接。
 Internet协议套件是企业网络和Internet中使用的主要网络协议。Internet协议套件表示分层网络协议的大型集合,它通常被称为TCP/IP,套件中包含的两个最重要的协议是:Internet协议(IP) 和 传输控制协议(TCP)。
 IPv6 和 IPv4表示Internet协议的两个可用版本。TCP是重要的网络服务之一,通常将其称为通过IPv6 和 IPv4网络运行的IP协议。用户数据报协议(UDP) 和 Internet控制消息协议(ICMP) 是用于IPv6 和 IPv4网络的其他重要IP协议。因此可通过IPv6 和 IPv4网络使用多种IP协议。
 Winsock将不同的网络协议套件视为不同的地址系列。如IPv6协议被视为AF_INET6地址系列,IPv4协议被视为AF_INET地址系列。IPv6 和 IPv4协议支持使用各种分层IP协议,例如TCP、UDP和ICMP。

三、使用Winsock

MSDN原文链接。
 本节介绍Winsock编程技术,包括基本的Winsock编程技术和高级技术。

3.1 关于服务器和客户端

  有两种不同类型的套接字(socket)网络应用程序:服务器 和 客户端。
 服务器和客户端具有不同的行为,因此它们的代码是不同的。下面是用于创建 流式处理TCP/IP服务器和客户端 的常规模型。

服务器客户端
1.初始化Winsock
2.创建套接字
3.绑定套接字3.连接到服务器
4.侦听客户端的套接字4.发送和接受数据
5.接受来自客户端的连接5.断开连接
6.接受和发送数据
7.断开连接

 注意:表中客户端和服务器的步骤不具有对应性。
 可以看出,客户端和服务器的处理模型中前两个步骤相同,这两个步骤的实现代码也几乎完全相同。
 本指南中的某些步骤是特定于要创建应用程序的类型而实现的。

3.2 创建基本Winsock应用程序

 步骤如下:

创建基本的Winsock应用程序
1.创建新的空项目,并将空的C++源文件添加到项目。
2.确保生成环境引用 Microsoft Windows软件开发工具包(SDK)。
3.确保生成环境链接到Winsock库文件Ws2_32.lib。使用Winsock的应用程序都必须与Ws2_32.lib库文件链接,可使用#pragma注释向链接器指示需要Ws2_32.lib文件。
4.开始对Winsock应用程序进行编程。包含Winsock2头文件即可使用Winsock API,Winsock2.h头文件包含了大多数Winsock的函数、结构和定义。包含Ws2tcpip.h头文件即可检索IP地址。

 经过以上步骤得到的源代码如下:

#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>

#pragma comment(lib, "Ws2_32.lib")

int main() {
  return 0;
}

 为了使用IP帮助应用程序使用API,需要包含lphlpapi.h头文件,并且其#include行应在Winsock.h头文件的#include行之后。如下所示:

#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h> // 放在winsock2.h之后
#include <stdio.h>

#pragma comment(lib, "Ws2_32.lib")

int main() {
  return 0;
}

 Winsock2.h头文件包含了Windows.h头文件中的核心元素,因此一般Winsock应用程序无需#include< Windows.h >。
 Windows.h中默认包含Windows socket 1.1版本的Winscok.h头文件。如果应用程序需要包含Windows.h头文件,那么为了避免Winsock.h中包含的声明与Window.h头文件相冲突,则应在包含Windows.h头文件之前定义WIN32_LEAN_AND_MEAN宏,它阻止了Windows.h包含Winsock.h。
 同时包含windows.h和winsock.h头文件的正确示例如下:

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN // 避免windows.h包含winsock 1.1
#endif

#include <windows.h>  // 先包含windows.h
#include <winsock2.h> // 再包含winsock2.h
#include <ws2tcpip.h>
#include <iphlpapi.h> // 最后包含iphlpapi.h

#pragma comment(lib, "Ws2_32.lib") // 链接Winscok库

int main() {
  return 0;
}

3.3 初始化Winscok

3.3.1 初始化步骤

 调用Winsock函数的所有进程都必须在调用之前初始化Windows套接字DLL的使用。初始化过程可以确认Winsock版本是否在用户系统上受支持。
 步骤一:创建WSADATA对象

WSADATA wsaData;

 步骤二:调用WSAStartup并检查其返回的整数值。

3.3.2 初始化的核心代码

int iResult; // 存储整数返回值

// 初始化Winsock
iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
if (iResult != 0) {
    printf("WSAStartup failed: %d\n", iResult);
    return 1;
}

 代码中WSAStartup的MSDN链接在此
 调用WSAStartup函数能启动WS2_32.dll的使用。WSADATA 结构包含有关 Windows 套接字实现的信息,WSAStartup会将传递的版本设置为调用方即用户可以使用的最高版本的Windows套接字支持。
 WSAStartup参数中的MAKEWORD(2,2)指明该程序最高支持的Winsock版本为2.2,如果初始化成功,该函数返回零,否则将返回一个可查询的错误码。
 WSAStartup函数必须是应用程序或DLL调用的第一个Winsock函数。它允许应用程序或DLL指定所需的Winsock版本,并检索特定Winsock实现的详细信息。

3.3.3 WSAStartup函数的协调

 Windows Socket 2向后兼容,WSAStartup函数会对应用程序所期望的winsock版本 和 用户Winsock DLL所支持的winsock版本进行协调。
 当应用程序或DLL调用WSAStartup函数时,Winsock DLL会检查函数参数中指定的Winsock版本,如果应用程序请求的版本即参数等于或高于Winsock DLL支持的最低版本,则调用成功。函数参数中的wsaData包含成员wVersion和wHighVersion,它们由Winsock DLL返回,分别代表协调结果 和 用户Winsock DLL所支持的最高版本。协调示例如下:
在这里插入图片描述

 假设程序请求2.2版本的winsock,而用户Winsock DLL支持的最低和最高版本分别为1.1和2.0,则表明应用程序所请求版本区间为【1.0-2.2】,而用户Winsock DLL所支持版本区间为【1.1-2.0】。由于2.2大于1.1,则两者交集必然不为空,因此也就存在二者都接受的winsock版本,所以WSAStartup返回0表示初始化成功,并且参数wsaData中返回了协调的结果,wsaData.wHighVersion返回用户Winsock DLL所支持的最高版本即2.0,wsaData.wVersion返回二者都接受的winsock版本即 min(用户期望版本2.2,Winsock DLL最高支持版本2.0) = 2.0。
 当前Winsock DLL即Ws2_32.dll支持的Winsock版本如下:

Winsock版本
1.0
1.1
2.0
2.1
2.2

 WSAStartup返回零仅表示有可用winscok版本,而不表示程序期望版本可用。如果你固定使用2.2版本的winsock,则在WSAStartup返回后,还需检查wsaData对象中的wVersion字段,因为如上例所示,它可能为2.0而非2.2,当且仅当它为2.2时才代表将使用2.2版本的Winscok。如果用户不支持较高如2.2版本的winsock,则可提醒用户安装更新版本的Winsock DLL。

3.3.4 WSACleanup函数

 应用程序使用完Winsock DLL的服务后,必须调用WSACleanup,以允许Winsock DLL释放应用程序使用的内部Winsock资源。
 如果需要多次获取WSADATA结构信息,应用程序可以多次调用WSAStartup。每次成功调用WSAStartup函数时,应用程序都必须调用WSACleanup函数,这意味着调用3次WSAStartup,则必须调用3次WSACleanup,而前两次WSACleanup除了递减内部计数器外什么也不做,最后一次WSACleanup调用才会将资源进行释放。

3.3.5 初始化的完整代码

 WSAStartup函数通常会导致加载特定于协议的帮助程序DLL,因此不应从程序DLL中的DllMain函数调用它,否则可能导致死锁。
 应用程序可以调用WSAGetLastError函数来确定winsock函数的扩展错误代码,它是Winsock 2.2 DLL中唯一可以在WSAStartup失败时调用的函数之一。
 根据上述内容,初始化Winsock,并确保用户程序使用的Winsock版本是2.2,对应代码如下:

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif 

#include <windows.h>
#include <winSock2.h>
#include <ws2tcpip.h>
#include <stdio.h>

#pragma comment(lib,"ws2_32.lib")

int __cdecl main()
{
	WORD mAppVersion; // 存储应用程序期望使用的winsock版本
	WSADATA wsaData; 
	int err;

	mAppVersion = MAKEWORD(2, 2); // 期望使用winsock 2.2
	
	err = WSAStartup(mAppVersion, &wsaData); // 初始化winscok
	if (err != 0) // 检查是否有可用版本
	{
		// 若返回非0表示错误,打印错误码
		printf("WSAStartup 错误的代码为:%d\n", err);
		return 1;
	}

	// 检查协调结果是否为winscok 2.2
	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
	{
		// 如果不是winsock 2.2,则打印找不到winsock 2.2
		printf("找不到可用版本的Winsock.dll\n");
		// 由于成功调用WSAStartup,则应调用WSACleanup
		WSACleanup();
		return 1;
	}
	else
		printf("发现Winsock 2.2 dll正常\n");

	// 若协调结果为winsock 2.2 ,程序最后也应调用WSACleanup
	WSACleanup();
}

3.4 Winsock客户端应用程序

3.4.1 为客户端创建套接字

3.4.1.1 客户端创建套接字代码

 回忆上文中的模型,服务器和客户端在初始化Winscok后都应创建套接字,但二者创建套接字的实现有所不同,现在介绍客户端创建套接字的实现。
 初始化Winsock后,必须实例化SOCKET对象以供客户端使用。其具体实现如下:

// 为客户端创建套接字
{
	// addrinfo即地址信息类型
	struct addrinfo* result = NULL, * ptr, hints;

	ZeroMemory(&hints, sizeof(hints)); // 将hints的内存块全部置为零
	hints.ai_family = AF_INET;		   // 指明调用方支持的地址系列为IPv4
	hints.ai_socktype = SOCK_STREAM;   // 支持的地套接字类型为流套接字
	hints.ai_protocol = IPPROTO_TCP;   // 支持的协议类型为TCP

#define DEFAULT_PORT "27015"

	// getaddrinfo函数提供与协议无关的从ANSI主机名到地址的转换
	iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
	if (iResult != 0) // 检查是否成功转换(即返回零)
	{
		// 失败则打印返回的非零Winsock错误代码
		printf("getaddrinfor 错误代码:%d\n", iResult);
		WSACleanup();
		return 1;
	}

	// 创建SOCKET对象
	SOCKET ConnectSocket = INVALID_SOCKET;

	ptr = result;

	// 调用socket函数并将其值返回到SOCKET对象中
	ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype,
		ptr->ai_protocol);

	// 检查套接字是否有效
	if (ConnectSocket == INVALID_SOCKET)
	{
		// 无效则打印错误号
		printf("Socket 错误代码:%ld\n", WSAGetLastError());
		freeaddrinfo(result);
		WSACleanup();
		return 1;
	}
}
3.4.1.2 getaddrinfo函数

addrinfo是用来存储地址信息的结构。通过addrinfo结构和getaddrinfo函数即可保存主机的地址信息。
getaddrinfo 函数提供与协议无关的从 ANSI 主机名到地址的转换。
 getaddrinfo函数的第一个参数:指向 以 NULL 结尾的 ANSI 字符串的指针,该字符串包含主机 (节点) 名称或数字主机地址字符串。 对于 Internet 协议,数字主机地址字符串是点十进制 IPv4 地址或 IPv6 十六进制地址。
 getaddrinfo函数的第二个参数:指向以 NULL 结尾的 ANSI 字符串的指针,该字符串包含表示为字符串的服务名称或端口号。服务名称是端口号的字符串别名。 例如,“http”是由 Internet 工程任务组定义的端口 80 的别名, (IETF) 作为 Web 服务器用于 HTTP 协议的默认端口。 以下文件中列出了未指定端口号时此参数的可能值:%WINDIR%\system32\drivers\etc\services。
 getaddrinfo函数的第三个参数:指向 addrinfo 结构的指针,该结构提供有关调用方支持的套接字类型的提示。此参数指向的 addrinfo 结构的ai_addrlen、ai_canonname、ai_addr和ai_next成员必须为零或 NULL。 否则, GetAddrInfoEx 函数将失败并 WSANO_RECOVERY。
 getaddrinfo函数的第四个参数:指向包含有关主机的响应信息的一个或多个 addrinfo 结构的链接列表的指针。此参数返回的所有信息都是动态分配的,包括所有 addrinfo 结构、套接字地址结构和 addrinfo 结构指向的规范主机名字符串。 成功调用此函数分配的内存必须通过后续调用 freeaddrinfo 释放。
 我们知道Socket是网络通信的一组API,服务器程序绑定到服务器主机IP地址的某个端口上,而客户端通过指定服务器的IP地址和端口向服务器发起连接请求。在getaddrinfo函数中,第一个参数可提供服务器的主机地址,第二个参数可提供服务器的端口号。
 通过调用getaddrinfo函数,即可根据命令行上传递的服务器主机地址argv[1]和端口号参数,获得函数存储在result变量中的服务器地址信息。

3.4.1.3 socket函数

 在上文代码中,我们首先创建了一个SOCKET对象并将其初始化为INVALID_SOCKET,这个枚举值表明此SOCKET对象的值无效。
 后续我们调用了socket函数,通过保存它的返回值为客户端创建了套接字。
 socket函数的三个参数分别为:地址系列规范、新套接字的类型规范、要使用的协议。如果未发生错误,则socket将返回引用新套接字的描述符,否则返回INVALID_SOCKET,并且可以通过调用WSAGetLastError来检索特定的错误代码。
 要重视错误检测,它是成功网络代码的关键部分。如果socket调用失败,将返回INVALID_SOCKET。我们将判断返回的SOCKET对象是否正确,若不正确则会调用WSAGetLastError,它会返回与上次发生错误相关联的错误号。
 可能需要进行更广泛的错误检查,即针对程序代码的检查,例如将hints.ai_family设置为AF_UNSPEC可能会导致连接调用失败,如果发生这种情况将其改为特定的IPv4或IPv6即可。
 WSACleanup将终止WS2_32 DLL的使用。

3.4.2 连接到套接字

 为客户端创建套接字后,要使得客户端能在网络上进行通信,则必须将其连接到服务器。
 调用connect函数,将创建的套接字即SOCKET对象和sockaddr结构作为参数传入,最后检查常规错误。实现代码如下:

// 连接到服务器
iResult = connect(ConnectSocket, ptr->ai_addr,
	(int)ptr->ai_addrlen);
if (iResult == SOCKET_ERROR)
{	
	// 如果连接失败则关闭socket并置SOCKET对象为无效值
	closesocket(ConnectSocket);
	ConnectSocket = INVALID_SOCKET;
}

// 无论是否成功连接,都释放返回的服务器地址信息
freeaddrinfo(result);

// 检查是否成功连接,若失败则打印连接失败
if (ConnectSocket == INVALID_SOCKET)
{
	printf("无法连接到服务器!\n");
	WSACleanup();
	return 1;
}

 注意ptr仅为result中的第一个指针,如果连接调用失败,应该尝试getaddrinfo返回的下一个地址,代码如下:

// 为客户端创建套接字

// addrinfo即地址信息类型
struct addrinfo* result = NULL, * ptr, hints;

ZeroMemory(&hints, sizeof(hints)); // 将hints的内存块全部置为零
hints.ai_family = AF_INET;		   // 指明调用方支持的地址系列为IPv4
hints.ai_socktype = SOCK_STREAM;   // 支持的地套接字类型为流套接字
hints.ai_protocol = IPPROTO_TCP;   // 支持的协议类型为TCP

#define DEFAULT_PORT "27015"

// getaddrinfo函数提供与协议无关的从ANSI主机名到地址的转换
iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
if (iResult != 0) // 检查是否成功转换(即返回零)
{
	// 失败则打印返回的非零Winsock错误代码
	printf("getaddrinfor 错误代码:%d\n", iResult);
	WSACleanup();
	return 1;
}

// 创建SOCKET对象
SOCKET ConnectSocket = INVALID_SOCKET;

ptr = result;

// 调用socket函数并将其值返回到SOCKET对象中
ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype,
	ptr->ai_protocol);

// 检查套接字是否有效
if (ConnectSocket == INVALID_SOCKET)
{
	// 无效则打印错误号
	printf("Socket 错误代码:%ld\n", WSAGetLastError());
	freeaddrinfo(result);
	WSACleanup();
	return 1;
}

while (ptr != NULL) // 只要指向服务器地址信息的指针不为空
{
	// 就尝试连接服务器
	iResult = connect(ConnectSocket, ptr->ai_addr,
		(int)ptr->ai_addrlen);

	// 如果连接失败则更换服务器的下一个地址信息,成功则退出循环
	if (iResult == SOCKET_ERROR)
		ptr = ptr->ai_next;
	else
		break;
}

// 检查是否连接成功,即最后一次连接是否返回非SOCKET_ERROR值
if (iResult == SOCKET_ERROR)
{
	// 如果连接服务器失败,则关闭socket,并将SOCKET对象设为无效值
	closesocket(ConnectSocket);
	ConnectSocket = INVALID_SOCKET;
}

// 无论成功与否,连接服务器完毕则释放动态分配的服务器地址信息result
freeaddrinfo(result);

// 检查最后是否连接到服务器,没有则打印提示
if (ConnectSocket == INVALID_SOCKET)
{
	printf("无法连接到服务器!\n");
	WSACleanup();
	return 1;
}

 getaddrinfo函数确定了sockaddr结构中的值,sockaddr结构中指定的信息包括:

  • 客户端将尝试连接到的服务器的IP地址。
  • 客户端将连接到的服务器上的端口号。

 getaddrinfo函数会返回一个addinfo链表,如果对第一个IP地址的连接失败,则请尝试下一个addrinfo结构。

3.4.3 在客户端上发送和接受数据

3.4.3.1 发送和接受数据的实现

 下面的代码将展示客户端成功连接服务器后,如何使用send和recv函数实现在客户端上收发数据。

#define DEFAULT_BUFLEN 512 // 定义默认缓冲区长度

// 将接收缓冲区的长度设置默认值
int recvbuflen = DEFAULT_BUFLEN;

// 定义客户端要发送给服务器的字符串
const char* sendbuf = "this is a test";
// 定义接受服务器信息的接受缓冲区
char recvbuf[DEFAULT_BUFLEN];

// 向服务器发送信息
iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);

// 检查是否发送成功,否则打印错误代码并关闭SOCK结束程序
if (iResult == SOCKET_ERROR)
{
	printf("发送错误代码:%d\n", WSAGetLastError());
	closesocket(ConnectSocket);
	WSACleanup();
	return 1;
}

// 打印send函数的返回值,即发送的字节数
printf("Bytes Send:%ld\n", iResult);

// 禁用套接字对象的发送操作
iResult = shutdown(ConnectSocket, SD_SEND);

// 查看禁用是否成功
if (iResult == SOCKET_ERROR)
{
	printf("shutdown faild:%d\n", WSAGetLastError());
	closesocket(ConnectSocket);
	WSACleanup();
	return 1;
}

// 循环接受服务器的消息
do {
	// 接受服务器的消息并存储在recvbuf中
	iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
	// 如果接受成功则打印接收到字节数
	if (iResult > 0)
		printf("Bytes received:%d\n", iResult);
	else if (iResult == 0) // 返回零表示连接断开
		printf("Connection closed\n");
	else // 返回负值表示错误代码
		printf("recv failed:%d\n", WSAGetLastError());
} while (iResult > 0); // 当且仅当接收成功时继续循环

 上述代码很容易理解,但我们不妨来深入了解以下其中涉及的winsock API即sendshutdownrecv

3.4.3.2 send函数

 send函数的功能是在连接的套接字上发送数据,其函数定义如下:
在这里插入图片描述
 如果未发生错误,send将返回发送的总字节数,该字节数可能小于len参数中请求发送的数量。如果发生错误则返回SOCKET_ERROR,并且可以通过调用WSAGetLastError检索特定的错误代码。

3.4.3.3 recv函数

 recv函数从连接的套接字或绑定的无连接套接字上接收数据。其函数定义如下:
在这里插入图片描述
 如果未发生错误,则recv函数返回接收到的字节数,buf参数指向的缓冲区将包含接收到的此数据。如果连接已正常关闭,则recv函数返回值为零。如果发生错误,返回SOCKET_ERROR,并且可通过调用 WSAGetLastError 来检索错误代码。

3.4.3.4 shutdown函数

 shutdown函数可禁用套接字的发送或接收等操作。其定义如下:
在这里插入图片描述
 如果未发生错误,则shutdown将返回零。否则返回SOCKET_ERROR,并且可通过调用 WSAGetLastError 来检索错误代码。

3.4.3.5 closesocket函数

closesocket函数关闭现有的套接字,如果未发生错误则返回零,否则返回SOCKET_ERROR,并且可通过调用 WSAGetLastError 来检索错误代码。
 对于每次成功调用套接字,应用程序应始终具有匹配的 closesocket 调用,以将任何套接字资源返回到系统。

3.4.4 断开客户端的连接

 客户端完成发送和接收数据后,客户端应该与服务器断开连接并关闭套接字。以下根据应用程序所处的两种情况,对断开连接进行实现。
 (1)当客户端完成发送数据的操作后,可以调用shutdown函数并指定SD_SEND关闭套接字的发送端,这将允许服务器释放此套接字的某些资源,并且客户端应用程序仍可接收套接字上的数据。实现如下:

iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR)
{
	printf("shutdown faild:%d\n", WSAGetLastError());
	closesocket(ConnectSocket);
	WSACleanup();
	return 1;
}

 (2)当客户端完成接收数据后,应调用closesocket函数以关闭套接字。应用程序若使用完Winsock DLL时,应使用WSACleanup函数来释放Winscok的所有资源,因此实现如下:

closesocket(ConnectSocket);
WSACleanup();

return 0;

3.4.5 完整的客户端实现

 完整的实现如下:

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif 

#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>

#pragma comment(lib,"Ws2_32.lib")
#pragma comment(lib,"Mswsock.lib")
#pragma comment(lib,"AdvApi32.lib")

#define DEFAULT_BUFLEN 512		// 定义默认缓冲大小
#define DEFAULT_PORT "27015"	// 定义服务器端口

int __cdecl main(int argc,char **argv)
{
	WSADATA wsaData;	// 保存winsock版本信息
	SOCKET ConnectSocket = INVALID_SOCKET;	// SOCKET对象
	struct addrinfo* result = NULL,			// 存储服务器地址信息
		* ptr = NULL, hints;
	const char* sendbuf = "this is a test"; // 存储客户端发送给服务器的字符串
	char recvbuf[DEFAULT_BUFLEN];			// 定义接收缓冲区
	int iResult;							
	int recvbuflen = DEFAULT_BUFLEN;		// 接收缓冲区大小为默认值

	if (argc != 2)	// 如果程序命令行不包含第二个参数,即不包含服务器主机的IP地址
	{
		// 则提示并结束程序
		printf("程序%s命令行中未使用服务器名称\n", argv[0]);
		return 1;
	}

	// 初始化Winsock
	iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != 0)
	{
		// 检查是否初始化成功,否则打印错误码,并结束程序
		printf("WSAStartup 错误码:%d\n", iResult);
		return 1;
	}

	// 这里可以检查wsaData.wVersion纪录的版本是否为2.2,以确保winsock DLL 2.2版本可用 

	// 将hints内存置零
	ZeroMemory(&hints, sizeof(hints));

	// hints记录了调用方即用户支持的套接字类型
	hints.ai_family = AF_UNSPEC;	 // 地址系列的支持未指定
	hints.ai_socktype = SOCK_STREAM; // 套接字类型支持流传输套接字
	hints.ai_protocol = IPPROTO_TCP; // 套接字协议支持TCP协议

	// 获取服务器的地址信息
	iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
	if (iResult != 0)
	{
		// 如果获取失败则打印错误代码
		// 由于初始化winsock成功,因此要调用WSACleanup,结束程序
		printf("getaddrinfo 错误码:%d\n", iResult);
		WSACleanup();
		return 1;
	}

	// 当获取服务器地址信息成功后,遍历服务器的每一个地址信息
	for (ptr = result; ptr != NULL; ptr = ptr->ai_next)
	{
		// 使用服务器的地址信息创建客户端的套接字
		ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype,
			ptr->ai_protocol);
		
		// 检查套接字是否创建成功
		if (ConnectSocket == INVALID_SOCKET)
		{
			// 失败则打印错误码
			printf("socket 错误码:%ld\n", WSAGetLastError());
			WSACleanup();
			return 1;
		}

		// 成功创建套接字,则连接服务器
		iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
		
		// 检查连接服务器是否成功
		if (iResult == SOCKET_ERROR)
		{
			// 由于成功创建套接字,所以连接失败需要关闭socket
			closesocket(ConnectSocket);
			// 将套接字重置为无效值,遍历服务器的下一个地址信息
			ConnectSocket = INVALID_SOCKET;
			continue;
		}

		// 连接服务器成功则退出遍历
		break;
	}

	// 无论连接服务器成功与否,释放服务器地址信息
	freeaddrinfo(result);

	// 判断是否连接成功,否则打印提示信息
	if (ConnectSocket == INVALID_SOCKET)
	{
		printf("无法连接到服务器!\n");
		WSACleanup();
		return 1;
	}

	// 连接成功则向服务器发送数据
	iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
	if (iResult == SOCKET_ERROR)
	{
		// 检查是否发送成功,否则关闭套接字,并结束程序
		printf("send 错误码: %d\n", WSAGetLastError());
		closesocket(ConnectSocket);
		WSACleanup();
		return 1;
	}

	printf("发送的字节数为: %ld\n", iResult);

	// 禁用客户端发送操作
	iResult = shutdown(ConnectSocket, SD_SEND);
	if (iResult == SOCKET_ERROR) 
	{
		// 检查禁用是否成功,否则关闭套接字,并结束程序
		printf("shutdown 错误码: %d\n", WSAGetLastError());
		closesocket(ConnectSocket);
		WSACleanup();
		return 1;
	}

	// 禁用成功则循环接收服务器消息
	do {
		// 接收服务器消息
		iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
		if (iResult > 0) // 接收成功
			printf("接收的字节数为: %d\n", iResult);
		else if (iResult == 0) // 连接关闭则提示
			printf("连接已关闭\n");
		else // 接收失败则打印错误信息
			printf("recv 错误码: %d\n", WSAGetLastError());
	} while (iResult > 0);	// 重复接收

	// 最后若连接正常关闭,则关闭套接字并结束程序
	closesocket(ConnectSocket);
	WSACleanup();

	return 0;
}

3.5 Winsock服务器应用程序

3.5.1 为服务器创建套接字

MSDN原文链接。
 服务器程序初始化Winsock后,也必须实例化SOCKET对象以供服务器使用。
 首先是调用getaddrinfo函数,服务器程序调用此函数的情况相对于客户端不同,具体实现如下:

// 将hints内存置零
	ZeroMemory(&hints, sizeof(hints));

	// hints记录了调用方即用户支持的套接字类型
	hints.ai_family = AF_INET;	 // 地址系列的支持为IPv4
	hints.ai_socktype = SOCK_STREAM; // 套接字类型支持流传输套接字
	hints.ai_protocol = IPPROTO_TCP; // 套接字协议支持TCP协议
	hints.ai_flags = AI_PASSIVE;

	// 获取服务器的地址信息
	iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
	if (iResult != 0)
	{
		// 如果获取失败则打印错误代码
		// 由于初始化winsock成功,因此要调用WSACleanup,结束程序
		printf("getaddrinfo 错误码:%d\n", iResult);
		WSACleanup();
		return 1;
	}

 可以看到在服务器程序中,我们设置了AI_PASSIVE标志,并且将getaddrinfo的第一个参数设置为NULL。设置AI_PASSIVE标志表示调用方打算在调用bind函数时使用返回的套接字地址结构,而同时设置AI_PASSIVE和NULL,则套接字地址结构的IP地址部分将设置为IPv4地址的INADDR_ANY,对于IPv6地址为IN6ADDR_ANY_INIT。
在这里插入图片描述
 接下来就是调用socket函数并存储其返回值。在客户端程序中,我们尝试getaddrinfo返回的所有的值作为socket函数的参数,而对于服务器程序仅使用getaddrinfo返回的第一个IP地址。
 本示例程序中指定套接字地址系列为IPv4,如果服务器想要侦听IPv6,则将hints中的地址系列设为AF_INET6即可。如果服务器想要同时侦听IPv4和IPv6,则必须创建两个侦听套接字,分别用于IPv4和IPv6,且应用程序必须对这两个套接字进行单独处理。
 为服务器创建套接字的实现如下:

	// 将hints内存置零
	ZeroMemory(&hints, sizeof(hints));

	// hints记录了调用方即用户支持的套接字类型
	hints.ai_family = AF_INET;	 // 地址系列的支持未指定
	hints.ai_socktype = SOCK_STREAM; // 套接字类型支持流传输套接字
	hints.ai_protocol = IPPROTO_TCP; // 套接字协议支持TCP协议
	hints.ai_flags = AI_PASSIVE;

	// 获取服务器的地址信息
	iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
	if (iResult != 0)
	{
		// 如果获取失败则打印错误代码
		// 由于初始化winsock成功,因此要调用WSACleanup,结束程序
		printf("getaddrinfo 错误码:%d\n", iResult);
		WSACleanup();
		return 1;
	}

	SOCKET ListenSocket = INVALID_SOCKET;

	ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);

	if (ListenSocket == INVALID_SOCKET)
	{
		printf("socket 错误码:%ld\n", WSAGetLastError());
		freeaddrinfo(result);
		WSACleanup();
		return 1;
	}

3.5.2 绑定套接字

 要使服务器能够接收客户端的连接,则它必须绑定到系统中的网络地址。下面将实现如何将已创建的套接字绑定到IP地址和端口,客户端使用此IP地址和端口连接到服务器主机网络。

iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);
if (iResult == SOCKET_ERROR)
{
	printf("bind 错误码:%d\n", WSAGetLastError());
	freeaddrinfo(result);      
	closesocket(ListenSocket);
	WSACleanup();
	return 1;
}

 调用bind函数后就不再需要getaddrinfo函数返回的地址信息了,因此需要调用freeaddrinfo函数释放getaddrinfo函数分配的内存。

3.5.3 侦听套接字

 服务器将套接字绑定到系统上的IP地址和端口后,必须侦听客户端传入连接请求的IP地址和端口。

if (listen(ListenSocket, SOMAXCONN) == SOCKET_ERROR)
{
	printf("Listen 错误码:%ld\n", WSAGetLastError());
	closesocket(ListenSocket);
	WSACleanup();
	return 1;
}

 调用listen函数,将套接字和一个整形作为参数传入。整形参数表示挂起的连接队列的最大长度,如果设置为SOMAXCONN,则表示允许最大合理的挂起连接数。

3.5.4 接收客户端连接

 在套接字侦听连接后,程序必须处理该套接字上的连接请求。

SOCKET ClientSocket = INVALID_SOCKET;

ClientSocket = accept(ListenSocket, NULL, NULL);
if (ClientSocket = INVALID_SOCKET)
{
	printf("accept 错误码:%d\n", WSAGetLastError());
	closesocket(ListenSocket);
	WSACleanup();
	return 1;
}

 在上述代码中,创建了名为ClientSocket的临时SOCKET对象,用于接收来自客户端的连接。
 通常服务器应用程序需要接收多个客户端的连接,对于高性能服务器而言,一般使用多个线程来处理多个客户端连接。
 Winsock有多种不同的编程技术可用于侦听多个客户端的连接。一种编程技术是创建一个连续循环,该循环使用listen函数检查客户端的连接请求。如果发生连接请求,服务器程序将调用accept函数,并将处理请求的工作传递给另一个线程。
 本实例没有使用多线程,因此仅侦听和接受单个连接。接受客户端连接后,服务区程序通常会将接受的客户端套接字如上例中的ClientSocket传递到工作线程,然后继续接收其他连接。在本例中,服务器将继续执行下一步操作。
 其他可用于侦听和接受多个连接的编程技术包括:selectWSAPoll函数等。

3.5.5 在服务器上接收和发送数据

 以下代码将演示服务器使用的recv和send函数。

char recvbuf[DEFAULT_BUFLEN];
int iResult, iSendResult;
int recvbuflen = DEFAULT_BUFLEN;

do {
	iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
	if (iResult > 0)
	{
		printf("接收的字节数为:%d\n", iResult);

		iSendResult = send(ClientSocket, recvbuf, iResult, 0);
		if (iSendResult == SOCKET_ERROR)
		{
			printf("send 错误码:%d\n", WSAGetLastError());
			closesocket(ClientSocket);
			WSACleanup();
			return 1;
		}
		printf("发送的字节数为:%d\n", iSendResult);
	}
	else if (iResult == 0)
		printf("连接关闭\n");
	else
	{
		printf("接收错误码:%d\n", WSAGetLastError());
		closesocket(ClientSocket);
		WSACleanup();
		return 1;
	}
} while (iResult > 0);

3.5.6 断开服务器连接

 服务器完成与客户端的收发数据后,服务器应与客户端断开连接并关闭套接字。

iResult = shutdown(ClientSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
	printf("shutdown 错误码: %d\n", WSAGetLastError());
	closesocket(ClientSocket);
	WSACleanup();
	return 1;
}

closesocket(ClientSocket);
WSACleanup();
return 0;

 上述代码调用shutdown函数关闭了服务器套接字的发送功能,这允许客户端释放此套接字的某些资源,且服务器程序仍可接收套接字上的数据。
 服务器最后应调用closesocket关闭套接字,并使用WSACleanup释放资源。

3.5.7 完整的服务器实现

 下面是基本Winsock TCP/IP 服务器应用程序的完整源代码:

#undef UNICODE

#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>

#pragma comment(lib,"Ws2_32.lib");

#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"

int __cdecl main(void)
{
	WSADATA wsaData;
	int iResult;

	SOCKET ListenSocket = INVALID_SOCKET;
	SOCKET ClientSocket = INVALID_SOCKET;

	struct addrinfo* result = NULL;
	struct addrinfo hints;

	int iSendResult;
	char recvbuf[DEFAULT_BUFLEN];
	int recvbuflen = DEFAULT_BUFLEN;

	// 初始化winsock
	iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != 0)
	{
		printf("WSAStartup failed with error: %d\n", iResult);
		return 1;
	}

	// 服务器创建套接字
	ZeroMemory(&hints, sizeof(hints));
	hints.ai_family = AF_INET;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = IPPROTO_TCP;
	hints.ai_flags = AI_PASSIVE;

	iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
	if (iResult != 0) {
		printf("getaddrinfo failed with error: %d\n", iResult);
		WSACleanup();
		return 1;
	}

	ListenSocket = socket(result->ai_family, result->ai_socktype,
		result->ai_protocol);
	if (ListenSocket == INVALID_SOCKET) {
		printf("socket failed with error: %ld\n", WSAGetLastError());
		freeaddrinfo(result);
		WSACleanup();
		return 1;
	}

	// 服务器绑定套接字
	iResult = bind(ListenSocket, result->ai_addr, result->ai_addrlen);
	if (iResult == SOCKET_ERROR) {
		printf("bind failed with error: %d\n", WSAGetLastError());
		freeaddrinfo(result);
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}

	// 释放getaddrinfo获取的地址信息的内存
	freeaddrinfo(result);

	// 侦听客户端的套接字
	iResult = listen(ListenSocket, SOMAXCONN);
	if (iResult == SOCKET_ERROR) {
		printf("listen failed with error: %d\n", WSAGetLastError());
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}

	// 接受来自客户端的连接
	ClientSocket = accept(ListenSocket, NULL, NULL);
	if (ClientSocket == INVALID_SOCKET) {
		printf("accept failed with error: %d\n", WSAGetLastError());
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}

	// 关闭侦听套接字
	closesocket(ListenSocket);

	// 服务器收发消息
	do {

		iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
		if (iResult > 0) {
			printf("Bytes received: %d\n", iResult);

			iSendResult = send(ClientSocket, recvbuf, iResult, 0);
			if (iSendResult == SOCKET_ERROR) {
				printf("send failed with error: %d\n", WSAGetLastError());
				closesocket(ClientSocket);
				WSACleanup();
				return 1;
			}
			printf("Bytes sent: %d\n", iSendResult);
		}
		else if (iResult == 0)
			printf("Connection closing...\n");
		else {
			printf("recv failed with error: %d\n", WSAGetLastError());
			closesocket(ClientSocket);
			WSACleanup();
			return 1;
		}

	} while (iResult > 0);

	// 断开服务区和客户端的连接
	iResult = shutdown(ClientSocket, SD_SEND);
	if (iResult == SOCKET_ERROR) {
		printf("shutdown failed with error: %d\n", WSAGetLastError());
		closesocket(ClientSocket);
		WSACleanup();
		return 1;
	}

	closesocket(ClientSocket);
	WSACleanup();

	return 0;
}

 若要使用完整服务器和客户端的代码进行实验,客户端应用程序必须在执行时,将计算机的名称或IP地址作为命令行参数传递给应用程序。

  • 17
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

仰望—星空

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值