【网络编程】TCP套接字编程

一、TCP套接字编程的基本步骤

流式套接字编程针对的是TCP 协议通信,即面向连接的通信,分为服务器端和客户端两 个部分,分别代表两个通信端点。下面看一下流式套接字编程的基本步骤。

服务器端编程的步骤

(1)加载套接字库(使用函数WSAStartup), 创建套接字(使用socket)。

(2)绑定套接字到一个IP 地址和一个端口上(使用函数bind)。

(3)将套接字设置为监听模式等待连接请求(使用函数listen), 这个套接字就是监听套 接字了。

(4)请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept)。

(5)用返回的新的套接字和客户端进行通信,即发送或接收数据(使用函数send 或 recv), 通信结束就关闭这个新创建的套接字(使用函数closesocket)。

(6)监听套接字继续处于监听状态,等待其他客户端的连接请求。

(7)如果要退出服务器程序,就先关闭监听套接字(使用函数closesocket), 再释放加 载的套接字库(使用函数WSACleanup)。

客户端编程的步骤:

(1)加载套接字库(使用函数WSAStartup), 创建套接字(使用函数 socket)。

(2)向服务器发出连接请求(使用函数connect)。

(3)和服务器端进行通信,即发送或接收数据(使用函数send 或 recv)。

(4)如果要关闭客户端程序,就先关闭套接字(使用函数closesocket), 再释放加载的 套接字库(使用函数WSACleanup)。

二、协议簇和地址簇

协议簇就是不同协议的集合。在Windows 中,用宏来表示不同的协议簇,这个宏的形式是以PF 开头的,比如 IPv4 协议簇为 PF INET,PF 的意思是 PROTOCOL FAMILY。

在WinSock2.h 中定义了不同协议的宏定义:

在这里插入图片描述

大家可以看到,各个协议宏由定义成了以AF 开头的宏,那么以AF 开头的宏又是哪路 神仙呢?其实,它就是地址簇的宏定义。地址簇就是一个协议簇所使用的地址集合(不同的网 络协议所使用的网络地址是不同的),也是用宏来表示不同的地址簇,这个宏的形式是以AF
开头的,比如IP 地址簇为AF INET,AF的意思是ADDRESS FAMILY。

在 ws2def.h中定义 了不同地址簇的宏定义:

在这里插入图片描述
在这里插入图片描述

现在,地址簇和协议簇的值其实是一样的,说到底都是用来标识不同的一套协议。那为何 会有两套东西呢?在很早以前,UNIX 有两种风格的系统,即BSD 系统和POSIX 系统:对于 BSD, 一直用的是AF ; 对 于POSIX, 一直用的是PF 。Windows 作为晚辈,不敢得罪两位 “大哥”,所以索性都支持它们了,这样两位“大哥”的一些应用软件稍加修改就都可以在 Windows 上编译了,说到底就是为了兼容。

既然这里说到“大哥”,必须雁过留名,否则就是不尊重,毕竟都是网络编程界的前辈。 很早以前,Bell 实验室的Ken Thompson开始利用一台闲置的 PDP-7 计算机开发了一种多用 户、多任务操作系统。很快,Dennis Richie加入了这个项目,在他们共同努力下诞生了最早的 UNIX。Richie 受一个更早的项目——MULTICS 的启发,将此操作系统命名为 UNIX。 早 期 UNIX 是用汇编语言编写的,但其第三个版本用一种崭新的编程语言C 重新设计了。C 是 Richie 设计出来并用于编写操作系统的程序语言。通过这次重新编写,UNIX 得以移植到更为强大的 DEC PDP-11/45与11/70计算机上运行。后来发生的一切,正如他们所说,已经成为历史。UNIX 从实验室走出来并成为操作系统的主流,现在几乎每个主要的计算机厂商都有其自有版本的 UNIX。随 着UNIX 成长,后来占领了市场,公司多了,懂的人也多了,就分家了。后来UNIX 太多太乱,大家编程接口甚至命令都不一样了,为了规范大家的使用和开发,就出现了POSIX 标准。典型的POSIX 标准的UNIX 实现有Solaris 、AIX 等。

BSD 代 表“Berkeley Software Distribution,伯克利软件套件”,是20世纪70年代加州大 学伯克利分校对贝尔实验室UNIX 进行一系列修改后的版本,最终发展成一个完整的操作系 统,有着自己的一套标准。现在,有多个不同的BSD 分支,并且“BSD” 并不特指任何一个 BSD 衍生版本,而是类UNIX 操作系统中的一个分支总称,典型的代表就是FreeBSD、NetBSD、 OpenBSD 等 。

三、socket地址

一个套接字代表通信的一端,每端都有一个套接字地址,这个socket 地址包含了IP 地址和端口信息。有了IP 地址,就能从网络中识别对方主机,有了端口就能识别对方主机上的进程。

socket 地址可以分为通用socket 地址和专用socket 地址。前者会出现在一些socket api函 数中(比如 bind 函 数 、connect 函数等),这个通用地址原来想用来表示大多数网络地址,但 现在有点不方便使用了,因此现在很多网络协议都定义自己的专用网络地址,专用网络地址主 要是为了方便使用而提出来的,两者通常可以相互转换。

3.1 通 用socket 地 址

通用socket地址就是一个结构体,名字是sockaddr,定义在ws2def.h中,该结构体如下:
在这里插入图片描述
其中,sa family 是一个无符号短整型(u short) 或枚举ADDRESS FAMILY 类型的变量, 用来存放地址簇(或协议簇)类型,常用取值如下:

  • PF_UNIX:UNIX本地域协议簇。
  • PF_INET:IPv4协议簇。
  • PF_INET6:IPv6协议簇。
  • AF_UNIX:UNIX 本地域地址簇。
  • AF_INET:IPv4 地址簇。
  • AF_INET6:IPv6地址簇。

sa_data用来存放具体的地址数据,即IP 地址数据和端口数据。
由于sa_data 只有14个字节,随着时代的发展, 一些新的协议提出来了,比如IPv6, 它 的地址长度不够14字节了。不同协议簇的具体地址长度见表5-1。

在这里插入图片描述

sa_data太小了,容纳不下了,咋办? Windows 定义了新的通用的地址存储结构:

在这里插入图片描述在这里插入图片描述

这个结构体存储的地址就大了,而且是内存对齐的,我们可以看到有 ss_align。

3.2 专用socket 地址

上面两个通用地址结构把IP 地址、端口等数据一股脑放到一个char 数组中,使得使用起 来特不方便。为此,Windows 为不同的协议簇定义了不同的 socket 地址结构体,这些不同的 socket 地址被称为专用socket 地址。比如,IPv4 有自己专用的socket 地 址 ,IPv6 有自己专用 的socket 地址。

IPv4 的 socket 地址定义了下面的结构体:

在这里插入图片描述

其中,类型IN_ADDR 在inaddr.h中定义如下:

在这里插入图片描述

其中,成员字段S_un 用来存放实际的IP 地址数据,是一个32位的联合体(联合体字段 S_un_b 有4个无符号char 型数据,因此取值32位;联合体字段S_un_w 有两个USHORT 型 数据,因此取值32位;联合体字段S_addr是 ULONG 型数据,因此取值也是32位)。

下面再来看一下IPv6 的 socket 地址专用结构体:
在这里插入图片描述

其中,类型IN6_ADDR在 in6addr.h 中定义如下:

在这里插入图片描述

这些专用的socket 地址结构体显然比通用的socket 地址更清楚,它把各个信息用不同的 字段来表示。需要注意的是,socket API函数使用的是通用地址结构,因此我们具体使用的时 候,最终要把专用地址结构转换为通用地址结构,不过可以强制转换。

3.3 IP地址的转换

IP 地址转换是指将点分十进制形式的字符串IP 地址与二进制IP 地址进行相互转换。比如, “192.168.1.100”就是一个点分十进制形式的字符串IP 地址。IP 地址转换可以通过 inet aton、 inet_addr和inet_ntoa这3个函数完成,这3个地址转换函数都只能处理IPv4地址,而不能处 理IPv6 地址。使用这些函数需要包含头文件Winsock2.h, 并加入库Ws2_32.lib。

函数 inet addr将点分十进制IP 地址转换为二进制地址,它返回的结果是网络字节序,该 函数声明如下:
在这里插入图片描述
其中,参数cp 指向点分十进制形式的字符串IP 地址,如“172.16.2.6”。如果函数成功 返回二进制形式的IP 地址,类型是32位无符号整型,失败则返回一个常值INADDR NONE (32位均为1)。通常失败的情况是参数cp 所指的字符串IP 地址不合法,比如“300.1000.1.1” (超过255了)。宏INADDR NONE在 ws2def.h 中定义如下:
在这里插入图片描述
下面我们再看看将结构体 in_addr 类 型 的 IP 地址转换为点分字符串 IP 地址的函数 inet_ntoa。注意,这里说的是结构体 in_addr 类型,即 inet_ntoa 函数的参数类型是 struct in_addr, 而不是inet_addr 返回的结果unsigned long类型,函数inet_ntoa 声明如下:

在这里插入图片描述
其中,in 存 放struct in_addr 类型的IP 地址。如果函数成功就返回字符串指针,此指针 指向转换后的点分十进制IP 地址,如果失败就返回NULL。

如果想要把 inet_addr 的结果再通过函数inet_ntoa 转换为字符串形式,怎么办呢?重要的 工作就是要将inet_addr 返回的unsigned long类型转换为struct in_addr 类型,可以这样:

在这里插入图片描述

s_addr 就 是S_un.S_addr(S_un.S_addr是ULONG 类型的字段),因此可以把dwIP 直接 赋值给ia.s addr, 然后把ia 传入 inet_ntoa 中,具体可以看下例。

在这里插入图片描述

代码很简单,先把IP172.16.2.6 通过函数inet_addr 转换为二进制并保存于ia.s_addr 中 , 然后以十六进制形式打印出来,接着通过函数inet_ntoa转换为点阵的字符串形式。

结果如下:

在这里插入图片描述

3.4 获取套接字地址

一个套接字绑定了地址就可以通过函数来获取它的套接字地址了。套接字通信需要本地和 远程两端建立套接字,这样获取套接字地址可以分为获取本地套接字地址和获取远程套接字地 址。其中,获取本地套接字地址的函数是getsockname, 这个函数在下面两种情况下可以获得 本地套接字地址:

(1)本地套接字通过bind函数绑定了地址(bind 函数在下一节会讲到)。

(2)本地套接字没有绑定地址,但通过 connect 函数和远程建立了连接,此时内核会分 配一个地址给本地套接字。

getsockname 函数声明如下:
在这里插入图片描述

其中,参数s 是套接字描述符;name 为指向存放套接字地址的结构体指针;namelen 是 name 所指结构体的大小。

绑定后获取本地套接字地址

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include<iostream>
#include<WinSock2.h>

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

//绑定获取本地套接字地址
void Test1() {
	int sfp;
	struct sockaddr_in s_add;
	struct sockaddr_in serv = { 0 };
	char on  = 1;

	int serv_len = sizeof(serv);

	WORD wVersionRequested;
	WSADATA wsadata;
	int err;
	unsigned short portnum = 10051;

	wVersionRequested = MAKEWORD(2, 2); //制作版本号

	//初始化winsock库
	err = WSAStartup(wVersionRequested, &wsadata);
	if (err != 0) {
		return;
	}

	sfp = socket(AF_INET, SOCK_STREAM, 0);

	if (sfp == -1) {
		std::cout << "socket fail" << std::endl;
		return ;
	}

	std::cout << "socket ok!" << std::endl;

	//还没绑定
	std::cout << "ip = " << inet_ntoa(serv.sin_addr) << " port = " << ntohs(serv.sin_port) << std::endl;
	
	//绑定socket
	setsockopt(sfp, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); //允许地址的立即重用

	memset(&s_add, 0, sizeof(struct sockaddr_in));
	s_add.sin_family = AF_INET;
	s_add.sin_addr.s_addr = inet_addr("127.0.0.1"); //这个ip必须在本机上有的
	s_add.sin_port = htons(portnum);

	//绑定
	if (bind(sfp, (struct sockaddr*)&s_add, sizeof(struct sockaddr)) == -1) {
		std::cout << "bind fail!" << std::endl;
		return;
	}


	std::cout << "bind ok!" << std::endl;

	getsockname(sfp, (struct sockaddr*)&serv, &serv_len); //获取本地套接字地址


	//打印套接字地址里的IP和端口值
	std::cout << "ip = " << inet_ntoa(serv.sin_addr) << " port = " << ntohs(serv.sin_port) << std::endl;

	WSACleanup(); //释放套接字库


}

int main() {
	Test1();

	return 0;
}

在上述代码中,我们首先创建了套接字,马上获取它的地址信息,然后绑定了IP 和端口 号,再去获取套接字地址。

可以看到没有绑定IP 和端口号前获取到的都是0,绑定后就可以正确获取到地址信息了。
需要注意的是,192.168.0.2必须是本机上存在的IP 地址,如果随便乱设一个并不存在的 IP 地址,程序会返回错误。大家可以修改一个并不存在的IP (比如0.0.0.0)地址后编译运行, 应该会出现下面的结果:

在这里插入图片描述

四、TCP套接字编程的相关函数

TCP套接字编程的相关函数由windows socket库 ( 简 称winsock 库)提供。该库分1.0和 2.0两个版本,现在主流是2.0版本。2.0版本的winsock API函数的声明在Winsock2.h中,在Ws2_32.dll 中实现。我们编程的时候需要包含头文件 Winsock2.h,同时要加入引用库 ws2_32.lib。

4.1 WSAStartup函数

该函数用于初始化Winsock DLL库,这个库提供了所有Winsock 函数,因此WSAStartup 必须要在所有Winsock 函数调用之前调用。函数声明如下:

在这里插入图片描述
其中,参数wVersionRequested 指明程序请求使用的Winsock 规范的版本,高位字节指明 副版本,低位字节指明主版本;参数lpWSAData 返回请求的Socket的版本信息,是一个指向 结构体WSADATA 的指针。如果函数成功就返回零,否则返回错误码。

结构体WSADATA 保存Windows 套接字的相关信息,定义如下:
在这里插入图片描述

当一个应用程序调用WSAStartup 函数时,操作系统根据请求的Winsock 版本来搜索相应 的 Winsock 库,然后绑定找到的Winsock 库到该应用程序中。以后应用程序就可以调用所请 求的Winsock 库中的函数了。比如一个程序要使用2.0版本的Winsock, 代码可以这样写:
在这里插入图片描述

4.2 socket/WSASocket函数

socket 函数用来创建一个套接字,声明如下:
在这里插入图片描述
其中,参数af 用于指定套接字所使用的协议簇(地址簇):对于IPv4 协议簇,该参数取 值为AF_INET(PF_INET); 对 于IPv6, 该参数取值为AF_INET6。当然不仅仅局限于这两 种协议簇,我们可以在ws2def.h 中看到其他的协议簇定义:
在这里插入图片描述

参 数 type 指定要创建的套接字类型:如果要创建流套接字类型,则取值为 SOCK_STREAM; 如果要创建数据报套接字类型,则取值为SOCK DGRAM; 如果要创建原 始套接字协议,则取值为 SOCK_RAW 。在 Winsock1.1 中,仅仅支持 SOCK_STREAM 和 SOCK_DGRAM; 到 了Winsock2, 就支持较多的套接字类型了,包括SOCK_RAW。在 ws2def.h 中定义了套接字类型的宏定义:

在这里插入图片描述

参数protocol 指定应用程序所使用的通信协议,即协议簇参数af所使用的上层(传输层) 协议,比如IPPROTO_TCP 表 示TCP 协 议 ,IPPROTO_UDP 表 示UDP 协议。这个参数通常和 前面两个参数都有关,如果该参数为0,就表示使用所选套接字类型对应的默认协议,比如如 果协议簇是AF INET, 套接字是SOCK_STREAM, 那么系统默认使用TCP 协议,而SOCK_DGRAM 套接字默认使用的协议是UDP。一般而言,给定协议簇和套接字类型,如果只支持 一种协议,那么用0没有问题;如果给定协议簇和套接字类型支持多种协议,就要指定协议参 数protocol 了。这一章我们进行的是TCP 编程,因此取IPPROTO_TCP 或0即可。如果函数 成功返回一个SOCKET 类型的描述符,那么该描述符可以用来引用新创建的套接字,如果失 败就返回INVALID_SOCKET, 可以使用函数WSAGetLastError 来获取错误码。

SOCKET的定义如下:
在这里插入图片描述

UINT PTR 其实是一个无符号整型,定义如下:
在这里插入图片描述

WSASocket函数是socket 的扩展版本,功能更为强大,通常用socket 即可。默认情况下, 这两个函数创建的套接字都是阻塞(模式)套接字。

4.3 bind函数

该函数让本地地址信息关联到一个套接字上,既可以用于连接的(流式)套接字,也可以 用于无连接的(数据报)套接字。当新建了一个Socket以后,套接字数据结构中有一个默认 的 IP 地址和默认的端口号。服务程序必须调用bind函数来给其绑定自己的IP 地址和一个特 定的端口号。客户程序一般不必调用bind函数来为其Socket绑 定IP 地址和端口号,客户端程 序通常会用默认的IP 和端口来与服务器程序通信。bind 函数声明如下:

在这里插入图片描述

其中,参数s 标识一个待绑定的套接字描述符;name 为指向结构体sockaddr 的指针,该 结构体包含了IP 地址和端口号;namelen 确定 name 的缓冲区长度。如果函数成功就返回零, 否则返回SOCKET ERROR。

结构体sockaddr 的定义如下:

在这里插入图片描述

这个结构体不是那么直观,所以人们又定义了一个新的结构:
在这里插入图片描述

这两个结构长度是一样的,所以可以相互强制转换。 结构in_addr 用来存储一个IP 地址,它定义如下:

在这里插入图片描述
我们通常习惯用点数的形式表示IP 地址,为此系统提供了函数 inet_addr, 将 IP 地址从点 数格式转换成网络字节格式。比如,已知IP 为223.153.23.45,我们把它存储到in_addr 中,可 以这样写:

在这里插入图片描述
我们对套接字进行绑定时,要注意设置的IP 地址是服务器真实存在的地址,不要输错。 比如服务器主机的IP 地址是192.168.1.2,而我们却设置绑定到了192.168.1.3上,此时bind 函数会返回错误:

在这里插入图片描述
这几行代码会打印:bind failed:10049。通过查询错误码10049得知,10049所代表的含义 是“Cannot assign requested address.”,意思就是不能分配所要求的地址,即IP 地址无效。因 此碰到这个错误码,大家应该多多注意是否把IP 地址写错了。同样,类似的代码在Linux 下 也是会报错误的(但错误码不同),如下所示:
在这里插入图片描述

这段代码在Linux 下输出“bind failed:99”,错 误 码errno 是99,虽然和Windows 下的错 误码不同,但代表的含义也是“Cannot assign requested address.”。总而言之,大家设置IP 地 址时要仔细小心。
能否不具体设置IP 地址,让系统去选一个可用的IP 地址呢?答案是肯定的,这也算是对 粗心之人的一种帮助吧,见下面这一行:

在这里插入图片描述

我们用“htonl(INADDR_ANY);" 替换了“inet addr(" 192.168.1.3");”, 其 中htonl 是把主 机字节序转为网络字节序,在网络上传输整型数据通常要转换为网络字节序。宏 INADDR_ANY告诉系统选取一个任意可用的IP 地址。

4.4 listen函数

该函数用于服务器端的流套接字,让流套接字处于监听状态,监听客户端发来的建立连接 的请求。该函数声明如下:

在这里插入图片描述
其中,参数s 是一个流套接字描述符,处于监听状态的流套接字s 将维护一个客户连接请 求队列;backlog 表示连接请求队列所能容纳的客户连接请求的最大数量,或者说队列的最大 长度。如果函数成功就返回零,否则返回SOCKET_ERROR。
举个例子,如果backlog 设置了5,当有6个客户端发来连接请求时,那么前5个客户端 连接会放在请求队列中,第6个客户端会收到错误。

4.5 accept/WSAAccept函数

accept 函数用于服务程序从处于监听状态的流套接字的客户连接请求队列中取出排在最 前的一个客户端请求,并且创建一个新的套接字来与客户套接字创建连接通道,如果连接成功, 就返回新创建的套接字的描述符,以后就用新创建的套接字与客户套接字相互传输数据。该函 数声明如下:

在这里插入图片描述
其中,参数s 为处于监听状态的流套接字描述符;addr 返回新创建的套接字的地址结构; addrlen 指向结构 sockaddr 的长度,表示新创建的套接字地址结构的长度。如果函数成功就 返回一个新的套接字的描述符,该套接字将与客户端套接字进行数据传输;如果失败就返回 INVALID_SOCKET。

下面的代码演示了accept 的使用:

在这里插入图片描述
WSAAccept函数是accept 的扩展版本。

4.6 connect/WSAConnect函数

connect 函数在套接字上建立一个连接。它用在客户端,客户端程序使用connect 函数请求 与服务器的监听套接字请求建立连接。该函数声明如下:
在这里插入图片描述

其 中 ,s 为还未连接的套接字描述符;name 是对方套接字的地址信息;namelen 是 name 所指缓冲区的大小。如果函数成功就返回零,否则返回SOCKET ERROR。
对于一个阻塞套接字,该函数的返回值表示连接是否成功,但如果连接不上通常要等较长 时间才能返回,此时可以把套接字设为非阻塞方式,然后设置连接超时时间。对于非阻塞套接 字,由于连接请求不会马上成功,因此函数会返回SOCKET_ERROR,但这并不意味着连接失 败,此时用函数WSAGetLastError 返回错误码将是WSAEWOULDBLOCK, 如果后续连接成 功了,就将获得错误码WSAEISCONN。
函数WSAConnect 为 connect 的扩展版本。

4.7 send/WSASend函数

send 函数用于在已建立连接的 socket 上发送数据,无论是客户端还是服务器应用程序都 用send 函数来向TCP 连接的另一端发送数据。但在该函数内部,它只是把参数 buf 中的数据 发送到套接字的发送缓冲区中,此时数据并不一定马上成功地被传到连接的另一端,发送数据 到接收端是底层协议完成的。该函数只是把数据发送(或称复制)到套接字的发送缓冲区后就 返回了。该函数声明如下:

在这里插入图片描述
其中,参数s 为发送端套接字的描述符;buf 存放应用程序要发送数据的缓冲区;len 表示 buf所指缓冲区的大小;flags一般设零。如果函数复制数据成功,就返回实际复制的字节数, 如果函数在复制数据时出现错误,那么 send 就返回SOCKET_ERROR。

如果底层协议在后续的数据发送过程中出现网络错误,那么下一个socket 函数就会返回 SOCKET_ERROR (这是因为每一个除send 外的 socket函数在执行的最开始总要先等待套接 字发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该 socket 函数就返回 SOCKET_ERROR)。

函数WSASend 是 send 的扩展函数。

4.8 recv/WSARecv函数

recv 函数从连接的套接字或无连接的套接字上接收数据,该函数声明如下:
在这里插入图片描述
该缓冲区用来存放从套接字的接收缓冲区中复制的数据;len 为 buf 所指缓冲区的大小;flags 一般设零。如果函数成功,就返回收到数据的字节数;如果连接被优雅地关闭了,那么函数返 回零;如果发生错误,就返回SOCKET ERROR。
函数WSARecv是 recv 的扩展版本。

4.9 closesocket函数

该函数用于关闭一个套接字。声明如下:
在这里插入图片描述

4.10 inet_addr函数

该函数用于将一个点分的字符串形式表示的IP 转换成无符号长整型。函数声明如下:
在这里插入图片描述

其中,参数cp 指向一个点分的IP 地址的字符串。如果函数成功就返回无符号长整型表示 的IP 地址,如果函数失败就返回INADDR_NONE。
下面的代码演示了函数inet_addr的使用:

在这里插入图片描述
也可以写成 “in.sin_addr.s_addr=dwip;”, 因 为 :
在这里插入图片描述

4.11 inet_ntoa 函数

该函数用于将一个in addr 结构类型的IP 地址转换成点分的字符串形式表示的IP 地址, 函数声明如下:

在这里插入图片描述
其中,参数in 是 in_addr 结构类型的IP 地址。如果函数成功就返回点分的字符串形式表
示的IP 地址,否则返回NULL。

4.12 htonl函数

该函数将一个u_long 类型的主机字节序转为网络字节序(大端)。函数声明如下
在这里插入图片描述
其中,参数hostlong 是要转为网络字节序的数据。函数返回网络字节序的hostlong。

4.13 htons 函数

该函数将一个u_short 类型的主机字节序转为网络字节序(大端)。函数声明如下:
在这里插入图片描述
其中,参数hostshort 是要转为网络字节序的数据。函数返回网络字节序的hostshort。

4.14 WSAAsyncSelect函数

该函数把某个套接字的网络事件关联到窗口,以便从窗口上接收该网络事件的消息通知。 这个函数用于实现非阻塞套接字的异步选择模型,允许应用程序以Windows 消息的方式接收 网络事件通知。该函数调用后会自动把套接字设为非阻塞模式,并且为套接字绑定一个窗口句 柄,当有网络事件发生时,便向这个窗口发送消息。函数声明如下:
在这里插入图片描述
其中,参数s 为网络事件通知所需的套接字描述符;hWnd 为当网络事件发生时,用于接 收消息的窗口句柄;wMsg 为网络事件发生时所接收到的消息;IEvent 为应用程序感兴趣的一 个或多个网络事件的比特组合码(或称位掩码)。如果函数成功就返回零,否则返回 SOCKET ERROR。
常见的套接字网络事件位掩码值如表5-2所示。

在这里插入图片描述
要注意的是 FD_WRITE,不是说发送数据时就会触发该事件,只是在连接刚刚建立,或 者发送缓冲区原先不够容纳所要发送的数据而现在空间够了,才触发该事件。
此外,可以通过消息wMsg 的消息参数IParam 来判断错误码和获取事件码。在Winsock2.h 中有这样的定义:

在这里插入图片描述
其中,通过 WSAGETSELECTERROR(IParam)可以判断是否发生错误,并且此时不能用WSAGetLastError 来获取错误码,要用 HIWORD(IParam)来获取错误码,错误码定义在 Winsock2.h中 ;LOWORD(IParam) 里存放了事件码,比如FD_READ、FD_WRITE 等。
另一个消息参数wParam 存放发生错误或事件的那个套接字。

4.15 WSACleanup函数

无论是客户端还是服务器端,当程序完成 Winsock 库的使用后,都要调用WSACleanup 函数来解除与Winsock 库的绑定并且释放Winsock 库所占用的系统资源。该函数声明如下:
在这里插入图片描述

如果函数成功就返回零,否则返回SOCKET ERROR。
TCP套接字编程可以分为阻塞套接字编程和非阻塞套接字编程。两种使用方式不同。

五、简单的TCP套接字编程

当使用函数socket 和 WSASocket 函数创建的套接字时,默认都是阻塞模式的。阻塞模式是指 套接字在执行操作时,调用函数在没有完成操作之前不会立即返回的工作模式。这意味着当调用 Winsock API不能立即完成时,线程处于等待状态,直到操作完成。常见的阻塞情况如下:

(1)接受连接函数
函数 accept/WSAAcept 从请求连接队列中接受一个客户端连接。如果以阻塞套接字为参数 调用这些函数,那么当请求队列为空时函数就会阻塞,线程将进入睡眠状态。

(2)发送函数
函 数send/WSASend 、sendto/WSASendto 都是发送数据的函数。当用阻塞套接字作为参数 调用这些函数时,如果套接字缓冲区没有可用空间,函数就会阻塞,线程就会睡眠,直到缓冲 区有空间。

(3)接收函数
函 数recv/WSARecv 、recvfrom/WSARecvfrom 用来接收数据。当用阻塞套接字为参数调用 这些函数时,如果套接字缓冲区没有数据可读,函数就会阻塞,调用线程在数据到来前将处于 睡眠状态。

(4)连接函数
函数connect/WSAConnect 用于向对方发出连接请求。客户端以阻塞套接字为参数调用这 些函数向服务器发出连接时,直到收到服务器的应答或超时才会返回。

使用阻塞模式的套接字开发网络程序比较简单,容易实现。在希望能够立即发送和接收数 据且处理的套接字数量较少的情况下,使用阻塞套接字模式来开发网络程序比较合适。它的不 足之处表现为:在大量建立好的套接字线程之间进行通信时比较困难。当希望同时处理大量套接字时将无从下手,扩展性差。

服务端:

#include <stdio.h>
#include <tchar.h>
#include <winsock.h>

#pragma comment(lib,"wsock32")

#define  BUF_SIZE  200
#define PORT 2048

int _tmain(int argc, _TCHAR* argv[])
{
	struct   sockaddr_in fsin;
	SOCKET    clisock;
	WSADATA  wsadata;
	int      alen, connum = 0;
	char     buf[BUF_SIZE] = "hi,client";

	struct  servent* pse;    /* server information    */
	struct  protoent* ppe;    /* proto information     */
	struct sockaddr_in sin;   /* endpoint IP address   */
	int  s;

	if (WSAStartup(MAKEWORD(2, 0), &wsadata) != 0)
	{
		puts("WSAStartup failed\n");
		WSACleanup();
		return -1;
	}
	memset(&sin, 0, sizeof(sin));
	sin.sin_family = AF_INET;
	sin.sin_addr.s_addr = INADDR_ANY;
	sin.sin_port = htons(PORT);

	/* get protocol number from protocol name */
	if ((ppe = getprotobyname("TCP")) == 0)
	{
		printf("  get protocol number error \n");
		WSACleanup();
		return -1;
	}


	s = socket(PF_INET, SOCK_STREAM, ppe->p_proto);
	if (s == INVALID_SOCKET)
	{
		printf(" creat socket error \n");
		WSACleanup();
		return -1;
	}

	if (bind(s, (struct sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
	{
		printf("  socket bind error \n");
		WSACleanup();
		return -1;
	}


	if (listen(s, 10) == SOCKET_ERROR)
	{
		printf("  socket listen error \n");
		WSACleanup();
		return -1;
	}

	while (1)
	{
		alen = sizeof(struct sockaddr);
		clisock = accept(s, (struct sockaddr*)&fsin, &alen);

		if (clisock == INVALID_SOCKET)
		{
			printf("initalize failed\n");
			WSACleanup();
			return -1;
		}
		connum++;
		send(clisock, buf, strlen(buf), 0);
		printf("%d  client  comes\n", connum);
		closesocket(clisock);
	}

	return 0;
}

客户端

#include <stdio.h>
#include <tchar.h>

#include <winsock.h>
#pragma comment(lib,"wsock32")

#define  BUF_SIZE  200
#define PORT 2048

int _tmain(int argc, _TCHAR* argv[])
{
	char  host[] = "localhost";

	char   buff[BUF_SIZE];
	SOCKET  s;
	int    len;
	WSADATA  wsadata;

	struct hostent* phe;      /*host information     */
	struct servent* pse;      /* server information  */
	struct protoent* ppe;     /*protocol information */
	struct sockaddr_in sin;   /*endpoint IP address  */
	int   type;


	if (WSAStartup(MAKEWORD(2, 0), &wsadata) != 0)
	{
		printf("WSAStartup failed\n");
		WSACleanup();
		return -1;
	}

	memset(&sin, 0, sizeof(sin));
	sin.sin_family = AF_INET;
	sin.sin_port = htons(PORT);
	//get IP address from  host name 
	if (phe = gethostbyname(host))
		memcpy(&sin.sin_addr, phe->h_addr, phe->h_length); /*  host IP address  */
	else if ((sin.sin_addr.s_addr = inet_addr(host)) == INADDR_NONE)
	{
		printf("get host IP information error \n");
		WSACleanup();
		return -1;
	}

	/**** get protocol number  from protocol name  ****/
	if ((ppe = getprotobyname("TCP")) == 0)
	{
		printf("get protocol information error \n");
		WSACleanup();
		return -1;
	}
	/**** creat a socket description ****/
	s = socket(PF_INET, SOCK_STREAM, ppe->p_proto);

	if (s == INVALID_SOCKET)
	{
		printf(" creat socket error \n");
		WSACleanup();
		return -1;
	}
	if (connect(s, (struct sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
	{
		printf("connect socket  error \n");
		WSACleanup();
		return -1;
	}
	while (0 == (len = recv(s, buff, sizeof(buff), 0)))
		;
	buff[len - 1] = '\0';
	printf("%s\n", buff);
	closesocket(s);
	WSACleanup();
	return 0;
}

在代码中,首先定义了和本机IP 同一子网的不真实存在的IP(120.4.6.99) 。 如果不是同 一 子网,connect 能很快判断出这个IP 不存在,所以超时时间较短。如果是同一子网的假IP, 则要等网关回复结果后connect 才知道是否能连通。如果将我们的电脑连上 Internet, 再用一个 公网上的假IP, 那么超时时间更长,因为要等很多网关、路由器等信息回复后connect 才能知 道是否可以连上。不过,现在我们同一子网里的假IP 用做测试就够了。

在这里插入图片描述

六、深入理解TCP 编程

6.1 数据发送和接收涉及的缓冲区

在发送端,数据从调用send 函数直到发送出去,主要涉及两个缓冲区:第一个是调用send 函数时程序员开辟的缓冲区,需要把这个缓冲区地址传给send 函数,这个缓冲区通常称为应 用程序发送缓冲区(简称为应用缓冲区);第二个缓冲区是协议栈自己的缓冲区,用于保存 send 函数传给协议栈的待发送数据和已经发送出去的数据但还没得到确认的数据,这个缓冲区通常称为TCP 套接字发送缓冲区(因为处于内核协议栈,所以有时也简称为内核缓冲区)。 数据从调用send 函数开始到发送出去,涉及两个主要写操作:第一个是把数据从应用程序缓 冲区中复制到协议栈的套接字缓冲区;第二个是从套接字缓冲区发送到网络上去。

数据在接收过程中也涉及两个缓冲区,首先数据达到的是TCP 套接字的接收缓冲区(也 就是内核缓冲区),在这个缓冲区中保存了TCP 协议从网络上接收到的与该套接字相关的数 据。接着,数据写到应用缓冲区,也就是调用 recv函数时由用户分配的缓冲区(也就是应用 缓冲区,这个缓冲区作为recv 参数),这个缓冲区用于保存从TCP 套接字的接收缓冲区收到 并提交给应用程序的网络数据。和发送端一样,两个缓冲区也涉及两个层次的写操作:从网络 上接收数据保存到内核缓冲区(TCP 套接字的接收缓冲区),然后从内核缓冲区复制数据到 应用缓冲区中。

6.2 TCP数据传输的特点

(1)TCP 是流协议,接收者收到的数据是一个个字节流,没有“消息边界”。

(2)应用层调用发送函数只是告诉内核我需要发送这么多数据,但不是说调用了发送函 数,数据马上就发送出去了。发送者并不知道发送数据的真实情况。

(3)真正可以发送多少数据由内核协议栈根据当前网络状态而定。

(4)真正发送数据的时间点也是由内核协议栈根据当前网络状态而定。

(5)接收端在调用接收函数时并不知道接收函数会实际返回多少数据。

6.3 数据发送的6种情形

知道了TCP 数据传输的特点,我们要进一步结合实际来了解发送数据时可能会产生的6 种情形。假设现在发送者调用了2次send 函数,分别先后发送了数据A 和数据B。我们站在
应用层来看,先调用send(A), 再调用send(B), 想当然地以为A 先送出了,然后是B。其 实 不一定如此。

(1)网络情况良好,A 和 B 的长度没有受到发送窗口、拥塞窗口和TCP 最大传输单元的 影响。此时协议栈将A 和 B 变成两个数据段发送到网络中。在网络中,它们如图5-4所示。

在这里插入图片描述

(2)发送A 的时候网络状况不好,导致发送A 被延迟,此时协议栈将数据A 和 B 合为 一个数据段后再发送,并且合并后的长度并未超过窗口大小和最大传输单元。在网络中,它们 如图5-5所示。

在这里插入图片描述

(3)A 发送被延迟了,协议栈把A 和 B 合为一个数据,但合并后数据长度超过了窗口大 小或最大传输单元。此时协议栈会把合并后的数据进行切分,假如B 的长度比A 大得多,则 切分的地方将发生在B 处,即协议栈把B 的部分数据进行切割,切割后的数据第二次发送。 在网络中,它们如图5-6所示。

在这里插入图片描述

(4)A 发送被延迟了,协议栈把A 和 B 合为一个数据,但合并后数据长度超过了窗口大 小或最大传输单元。此时协议栈会把合并后的数据进行切分,如果A 的长度比B 大得多,则 切分的地方将发生在A 处,即协议栈把A 的部分数据进行切割,切割后的部分A 先发送,剩 下的部分A 和 B 一起合并发送。在网络中,它们如图5-7所示。

在这里插入图片描述

(5)接收方的接收窗口很小,内核协议栈会将发送缓冲区的数据按照接收方的接收窗口 大小进行切分后再依次发送。在网络中,它们如图5-8所示。

在这里插入图片描述

(6)发送过程发生了错误,数据发送失败。

6.4 数据接收时碰到的情形

前面说了发送数据的时候,内核协议栈在处理发送数据时可能会出现6种情形。现在我们 来看接收数据时会碰到哪些情况。对于本次接收函数 recv 应用缓冲区足够大,它调用后,通 常有以下几种情况:

第一,接收到本次达到接收端的全部数据。

注意,这里的全部数据是已经达到接收端的全部数据,不是说发送端发送的全部数据,即 本地到达多少数据,接收端就接收本次全部数据。我们根据发送端的几种发送情况来推导达到 接收端的可能情况:

  • 对于发送端(1)的情况,如果到达接收端的全部数据是A, 则接收端应用程序就全 部收到了A。
  • 对于发送端(2)的情况,如果到达接收端的全部数据是A 和 B, 则接收端应用程序 就全部收到了A 和 B。
  • 对于发送端(3)的情况,如果到达接收端的全部数据是A 和 B1, 则接收端应用程 序就全部收到了A 和 B1。
  • 对于发送端(4)和(5)的情况,如果到达接收端的全部数据是部分A, 比 如 ( 4 ) 中 A1 是部分A,(5) 中开始的一个矩形条也是部分A, 则接收端应用程序收到的 是部分A。

第二,接收到达到接收端数据的部分。

如果接收端的应用程序的接收缓冲区较小,就有可能只收到已达到接收端的全部数据中的 部分数据。

综上所述,TCP 网络内核如何发送数据与应用层调用send 函数提交给TCP 网络核没有直 接关系。我们也无法对接收数据的返回时机和接收到的数量进行预测,为此需要在编程中做正 确处理。另外,在使用TCP 开发网络程序的时候,不要有“数据边界”的概念,TCP 是一个 流协议,没有数据边界的概念。这几点值得我们在开发TCP 网络程序时多加注意。

第三,没有接收到数据。

表明接收端接收的时候,数据还没有准备好。此时,应用程序将阻塞或recv返回一个“数 据不可得”的错误码。通常这种情况发生在发送端出现(6)的那种情况,即发送过程发生了 错误,数据发送失败。

通过上面TCP 发送和接收的分析,我们可以得出2个“无关”结论,这个“无关”也可 理解为独立。

(1)应用程序调用send函数的次数和内核封装数据的个数是无关的。

(2)对于要发送的一定长度的数据而言,发送端调用send 函数的次数和接收端调用recv 函数的次数是无关的,完全独立的。比如,发送端调用一次 send 函数,可能接收端会调用多 次recv函数来接收。同样,接收端调用一次recv函数也可能收到的是发送端多次调用send后

了解了接收会碰到的情况后,我们写程序时,就要合理地处理多种情况。首先,我们要能 正确地处理接收函数recv 的返回值。我们来看一下 recv 函数的调用形式:

在这里插入图片描述

如果没有出现错误,recv 返回接收的字节数,buf 参数指向的缓冲区将包含接收的数据。 如果连接已正常关闭,那么返回值为零,即res 为0。如果出现错误,就将返回SOCKET_ERROR 的值,并且可以通过调用函数WSAGetLastError 来获得特定的错误代码。

6.5 一次请求响应的数据接收

一次请求响应的数据接收,就是接收端接收完全部数据后接收结束,发送端断开连接。我 们可以通过连接是否关闭来知道数据接收是否结束。

对于单次数据接收(调用一次recv 函 数 ) 来 讲 ,recv 返回的数据量是不可预测的,也就 无法估计接收端在应用层开设的缓冲区是否大于发来的数据量大小,因此我们可以用一个循环 的方式来接收。我们可以认为recv 返回0就是发送方数据发送完毕了,然后正常关闭连接。 其他情况,我们就要不停地去接收数据,这样数据就不会漏收了。接着我们来看一个例子。当 客户端连接服务器端成功后,服务器端先向客户端发一段信息,客户端接收后,再向服务器端 发一段信息,最后客户端关闭连接。这一来一回相当于一次聊天。其实,以后开发更完善的点 对点的聊天程序可以基于这个例子。我们使用小例子,主要是为了演示清楚原理细节。

一个稍完善的服务器客户机通信程序

服务端:

#define _WINSOCK_DEPRECATED_NO_WARNINGS // 为了使用inet_ntoa时不出现警告
#include <Winsock2.h>
#include<cstdio>

#pragma comment(lib, "ws2_32.lib") //Winsock库的引入库
#define BUF_LEN 300

int main(){
	WORD wVersionRequested;
	WSADATA wsaData;
	int err, i, iRes;

	wVersionRequested = MAKEWORD(2, 2); //制作Winsock库的版本号

	err = WSAStartup(wVersionRequested, &wsaData); //初始化Winsock库
	if (err != 0) return 0;

	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) //判断返回的版本号是否正确
	{
		WSACleanup();
		return 0;
	}
	SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0); //创建一个套接字,用于监听客户端的连接

	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); //使用当前主机任意可用IP
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(8000);  //使用端口8000

	bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); //绑定
	listen(sockSrv, 5); //监听

	SOCKADDR_IN addrClient;
	int len = sizeof(SOCKADDR);

	while (1){
		printf("--------等待客户端-----------\n");
		//从连接请求队列中取出排在最前的一个客户端请求,如果队列为空就就阻塞
		SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, &len);
		char sendBuf[100] = "";
		for (i = 0; i < 10; i++){
			sprintf_s(sendBuf, "N0.%d欢迎登录服务器,请问1+1等于几?(客户端IP:%s)", i + 1, inet_ntoa(addrClient.sin_addr));//组成字符串
			send(sockConn, sendBuf, strlen(sendBuf), 0); //发送字符串给客户端
			memset(sendBuf, 0, sizeof(sendBuf));
		}

		// 数据发送结束,调用shutdown()函数声明不再发送数据,此时客户端仍可以接收数据
		iRes = shutdown(sockConn, SD_SEND);
		if (iRes == SOCKET_ERROR) {
			printf("shutdown failed with error: %d\n", WSAGetLastError());
			closesocket(sockConn);
			WSACleanup();
			return 1;
		}

		//发送结束,开始接收客户端发来的信息
		char recvBuf[BUF_LEN];

		// 持续接收客户端数据,直到对方关闭连接 
		do {

			iRes = recv(sockConn, recvBuf, BUF_LEN, 0);
			if (iRes > 0) //成功收到消息
			{
				printf("\nRecv %d bytes:", iRes);
				for (i = 0; i < iRes; i++)
					printf("%c", recvBuf[i]);
				printf("\n");
			}
			else if (iRes == 0)
				printf("\n客户端关闭连接了\n");
			else
			{
				printf("recv failed with error: %d\n", WSAGetLastError());
				closesocket(sockConn);
				WSACleanup();
				return 1;
			}

		} while (iRes > 0);


		closesocket(sockConn); //关闭和客户端通信的套接字
		puts("是否继续监听?(y/n)");
		char ch[2];
		scanf_s("%s", ch, 2); //读控制台两个字符,包括回车符
		if (ch[0] != 'y') //如果不是y就退出循环
			break;
	}

	closesocket(sockSrv); //关闭监听套接字
	WSACleanup(); //释放套接字库
	return 0;
}

客户端

#define _WINSOCK_DEPRECATED_NO_WARNINGS // 为了使用inet_ntoa时不出现警告
#include<cstdio>
#include <Winsock2.h>
#pragma comment(lib, "ws2_32.lib")

#define BUF_LEN 300

int main(){
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	u_long argp;
	char szMsg[] = "你好,服务器,我已经收到你的信息";

	wVersionRequested = MAKEWORD(2, 2); //初始化Winsock库

	err = WSAStartup(wVersionRequested, &wsaData);
	if (err != 0) return 0;

	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) //判断返回的版本号是否正确
	{
		WSACleanup();
		return 0;
	}
	SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);//新建一个套接字

	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //服务器的IP
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(8000); //服务器的监听端口
	err = connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); //向服务器发出连接请求
	if (SOCKET_ERROR == err) //判断连接是否成功
	{
		printf("连接服务器失败,请检查服务器是否启动\n");
		return 0;
	}

	char recvBuf[BUF_LEN];
	int i, cn = 1, iRes;
	do{
		iRes = recv(sockClient, recvBuf, BUF_LEN, 0); //接收来自服务器的信息
		if (iRes > 0)
		{
			printf("\nRecv %d bytes:", iRes);
			for (i = 0; i < iRes; i++)
				printf("%c", recvBuf[i]);
			printf("\n");
		}
		else if (iRes == 0)//对方关闭连接
			puts("\n服务端关闭发送连接了。。。\n");
		else
		{
			printf("recv failed:%d\n", WSAGetLastError());
			printf("recv failed with error: %d\n", WSAGetLastError());
			closesocket(sockClient);
			WSACleanup();
			return 1;
		}

	} while (iRes > 0);


	//开始向客户端发送数据
	char sendBuf[100];
	for (i = 0; i < 10; i++)
	{
		sprintf_s(sendBuf, "N0.%d我是客户端,1+1=2 ", i + 1);//组成字符串
		send(sockClient, sendBuf, strlen(sendBuf) + 1, 0); //发送字符串给客户端
		memset(sendBuf, 0, sizeof(sendBuf));
	}
	puts("向服务端发送数据完成");
	closesocket(sockClient); //关闭套接字
	WSACleanup(); //释放套接字库
	system(0);


	return 0;
}

客户端接收也用了循环结构,这样能正确处理接收时的情况(根据recv 的返回值)。数 据接收完毕后,也多次调用send 函数向服务器端发送数据,发送完毕后调用closesocket 来关 闭套接字,这样服务器端就不会阻塞在recv那里死等了。

在这里插入图片描述
看到服务器端一共接收了2次数据,第一次收到了23字节,第二次接收到了208字节。 客户端发来的数据都接收下来了。

可以看到,客户端一共接收了3次数据,第一次收到了58字节数据,第二次收到了300 字节,第三次收到了223字节数据。服务器端发来的全部数据都接收下来了。

6.6 多次请求响应的数据接收

多次请求响应的数据接收就是接收端要多轮接收数据,每轮接收又包含循环多次接收, 一 轮接收完毕后,连接并不断开,而是等到多轮接收完毕后才断开连接。在这种情况下,我们的 循环接收中不能用recv返回值是否为0来判断连接是否结束了,当然可以作为条件之一,还 要增加一个条件,那就是本轮是否全部接收完应接收的数据了。该如何判断呢?

有两种方法,第一种方法是通信双方约定好发送数据的长度,这种方法也称定长数据的接 收。比如发送方告诉接收方,我要发送n 字节的数据,发完我就断开连接了。那么接收端就要 等n 字节数据全部接收完后才能退出循环,表示接收完毕。下面看一个例子,服务器给客户端 发送约定好的固定长度(比如250字节)的数据后并不断开连接,而是等待客户端的接收成功 确认信息。此时,客户端就不能根据连接是否断开来判断接收是否结束了(当然,连接是否断 开也要进行判断,因为可能会有意外出现),而是要根据是否接收完250字节来判断了,接收 完毕后,再向服务器端发送确认消息。这个过程相当于一个简单的、相互约好的交互协议了。

接收定长数据

服务端


#define _WINSOCK_DEPRECATED_NO_WARNINGS // 为了使用inet_ntoa时不出现警告
#include <Winsock2.h>
#include<stdio.h>
#pragma comment(lib, "ws2_32.lib") //Winsock库的引入库
#define BUF_LEN 300

int main()
{
	WORD wVersionRequested;
	WSADATA wsaData;
	int err, i, iRes;

	wVersionRequested = MAKEWORD(2, 2); //制作Winsock库的版本号

	err = WSAStartup(wVersionRequested, &wsaData); //初始化Winsock库
	if (err != 0) return 0;

	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) //判断返回的版本号是否正确
	{
		WSACleanup();
		return 0;
	}
	SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0); //创建一个套接字,用于监听客户端的连接

	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); //使用当前主机任意可用IP
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(8000);  //使用端口8000

	bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); //绑定
	listen(sockSrv, 5); //监听

	SOCKADDR_IN addrClient;
	int cn = 0, len = sizeof(SOCKADDR);

	while (1)
	{
		printf("--------等待客户端-----------\n");
		//从连接请求队列中取出排在最前的一个客户端请求,如果队列为空就就阻塞
		SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, &len);
		char sendBuf[111] = "";

		for (cn = 0; cn < 50; cn++)
		{
			memset(sendBuf, 'a', 111);
			if (cn == 49)
				sendBuf[110] = 'b';
			send(sockConn, sendBuf, 111, 0); //发送字符串给客户端
		}
		//发送结束,开始接收客户端发来的信息
		char recvBuf[BUF_LEN];

		// 持续接收客户端数据,直到对方关闭连接 
		do {


			iRes = recv(sockConn, recvBuf, BUF_LEN, 0);
			if (iRes > 0)
			{
				printf("\nRecv %d bytes:", iRes);
				for (i = 0; i < iRes; i++)
					printf("%c", recvBuf[i]);
				printf("\n");
			}
			else if (iRes == 0)
				printf("\n客户端关闭连接了\n");
			else
			{
				printf("recv failed with error: %d\n", WSAGetLastError());
				closesocket(sockConn);
				WSACleanup();
				return 1;
			}

		} while (iRes > 0);



		closesocket(sockConn); //关闭和客户端通信的套接字
		puts("是否继续监听?(y/n)");
		char ch[2];
		scanf_s("%s", ch, 2); //读控制台两个字符,包括回车符
		if (ch[0] != 'y') //如果不是y就退出循环
			break;
	}
	closesocket(sockSrv); //关闭监听套接字
	WSACleanup(); //释放套接字库
	return 0;
}

在上面的代码中,我们向客户端一共发送5550字节的数据,每次发送111个,一共发送 50次。这个长度是和服务器端约好的,发完固定的5550字节后,并不关闭连接,而是继续等 待客户端的消息,但不要想当然认为客户端每次收到的都是111个。

客户端

#define _WINSOCK_DEPRECATED_NO_WARNINGS // 为了使用inet_ntoa时不出现警告
#include <Winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

#define BUF_LEN 250

int main()
{
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	u_long argp;
	char szMsg[] = "你好,服务器,我已经收到你的信息";

	wVersionRequested = MAKEWORD(2, 2); //初始化Winsock库

	err = WSAStartup(wVersionRequested, &wsaData);
	if (err != 0) return 0;

	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) //判断返回的版本号是否正确
	{
		WSACleanup();
		return 0;
	}
	SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);//新建一个套接字

	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //服务器的IP
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(8000); //服务器的监听端口
	err = connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); //向服务器发出连接请求
	if (SOCKET_ERROR == err) //判断连接是否成功
	{
		printf("连接服务器失败,请检查服务器是否启动\n");
		return 0;
	}
	char recvBuf[BUF_LEN];
	int i, cn = 1, iRes;
	int leftlen = 50 * 111;//这个5550是通信双方约好的
	while (leftlen > 0)
	{
		iRes = recv(sockClient, recvBuf, BUF_LEN, 0); //接收来自服务器的信息
		if (iRes > 0)
		{
			printf("\nNo.%d:Recv %d bytes:", cn++, iRes);
			for (i = 0; i < iRes; i++)
				printf("%c", recvBuf[i]);
			printf("\n");
		}
		else if (iRes == 0)//对方关闭连接
			puts("\n服务端关闭发送连接了。。。\n");
		else
		{
			printf("recv failed:%d\n", WSAGetLastError());
			printf("recv failed with error: %d\n", WSAGetLastError());
			closesocket(sockClient);
			WSACleanup();
			return 1;
		}
		leftlen = leftlen - iRes;
	}



	char sendBuf[100];
	sprintf_s(sendBuf, "我是客户端,我已经完成数据接收了");//组成字符串
	send(sockClient, sendBuf, strlen(sendBuf) + 1, 0); //发送字符串给客户端
	memset(sendBuf, 0, sizeof(sendBuf));

	puts("向服务端发送数据完成");
	closesocket(sockClient); //关闭套接字
	WSACleanup(); //释放套接字库
	system(0);


	return 0;
}

在这里插入图片描述

在代码中,我们定义了一个变量leftlen, 用来表示还有多少数据没有接收,开始的时候是 5550字节(和服务器端约好的数字),以后每次接收一部分就减去已经接收到的数据。直到 等于0,就全部接收完毕。


通常有两种方法可以知道要接收多少变长数据。
(1)第一种方法是每个不同长度的数据包末尾跟一个结束标识符,接收端在接收的时候, 一旦碰到结束标识符,就知道当前的数据包结束了。这种方法必须保证结束符的唯一性,而且 效率比较低,所以不常用。结束符的判断方式在实际项目中貌似不受欢迎,因为得扫描每个字 符才行。

(2)第二种方法是在变长的消息体之前加一个固定长度的包头,包头里放一个字段,用 来表示消息体的长度。接收的时候,先接收包头,然后解析得到消息体长度,再根据这个长度 来接收后面的消息体。

具体开发时,我们可以定义这样的结构体:

在这里插入图片描述

其中,nLen 用来标识消息体的长度;data 是一个数组名,但该数组没有元素,真实地址 紧随结构体MyData 之后,而这个地址就是结构体后面数据的地址(如果给这个结构体分配的 内容大于这个结构体实际大小,后面多余的部分就是这个data的内容)。这种声明方法可以 巧妙地实现C 语言里的数组扩展。
实际用时采取如下形式:

在这里插入图片描述

这样就可以通过p->data 来操作这个str。在这里先插入一个小例子,让大家熟悉一下 data[0] 的用法,在网络程序中不至于用错。基础不牢,地动山摇。
在这里插入图片描述
在这里插入图片描述
由这个例子可知,data 的地址是紧随结构体之后的。相信通过这个小例子,大家对结构体 中data[0]的用法有所了解了。下面我们可以把它运用到网络程序中去了。

服务端


#define _WINSOCK_DEPRECATED_NO_WARNINGS // 为了使用inet_ntoa时不出现警告
#include <Winsock2.h>
#include<cstdio>

#pragma comment(lib, "ws2_32.lib") //Winsock库的引入库
#define BUF_LEN 300

struct MyData
{
	int nLen;
	char data[0];
};

int main()
{
	WORD wVersionRequested;
	WSADATA wsaData;
	int err, i, iRes;

	wVersionRequested = MAKEWORD(2, 2); //制作Winsock库的版本号

	err = WSAStartup(wVersionRequested, &wsaData); //初始化Winsock库
	if (err != 0) return 0;

	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) //判断返回的版本号是否正确
	{
		WSACleanup();
		return 0;
	}
	SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0); //创建一个套接字,用于监听客户端的连接

	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); //使用当前主机任意可用IP
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(8000);  //使用端口8000

	bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); //绑定
	listen(sockSrv, 5); //监听

	SOCKADDR_IN addrClient;
	int cn = 0, len = sizeof(SOCKADDR);
	struct MyData* mydata;
	while (1)
	{
		printf("--------等待客户端-----------\n");
		//从连接请求队列中取出排在最前的一个客户端请求,如果队列为空就就阻塞
		SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, &len);

		cn = 5550;
		{
			mydata = (MyData*)malloc(sizeof(MyData) + cn);
			mydata->nLen = htonl(cn);
			memset(mydata->data, 'a', cn);
			mydata->data[cn - 1] = 'b';
			send(sockConn, (char*)mydata, sizeof(MyData) + cn, 0); //发送字符串给客户端
			free(mydata);
		}
		//发送结束,开始接收客户端发来的信息
		char recvBuf[BUF_LEN];

		// 持续接收客户端数据,直到对方关闭连接 
		do {

			iRes = recv(sockConn, recvBuf, BUF_LEN, 0);
			if (iRes > 0)
			{
				printf("\nRecv %d bytes:", iRes);
				for (i = 0; i < iRes; i++)
					printf("%c", recvBuf[i]);
				printf("\n");
			}
			else if (iRes == 0)
				printf("\n客户端关闭连接了\n");
			else
			{
				printf("recv failed with error: %d\n", WSAGetLastError());
				closesocket(sockConn);
				WSACleanup();
				return 1;
			}

		} while (iRes > 0);



		closesocket(sockConn); //关闭和客户端通信的套接字
		puts("是否继续监听?(y/n)");
		char ch[2];
		scanf_s("%s", ch, 2); //读控制台两个字符,包括回车符
		if (ch[0] != 'y') //如果不是y就退出循环
			break;
	}
	closesocket(sockSrv); //关闭监听套接字
	WSACleanup(); //释放套接字库
	return 0;
}

际发送的是5550+4),4是长度字段的字节数,只不过这个长度是发送端设定的,没和接 收端约好。所以我们定义了一个结构体,结构体的头部整型字段nLen 表示消息体的长度(这 里是5550)。由于我们采用了0数组,所以分配的空间是连续的,因此send的时候,可以将 结构体地址作为参数代入send 函数,但注意长度是sizeof(MyData)+cn,表示长度字段的长和 消息体的长。
这样发送出去后,接收端那里先接收4字节的长度字段,然后知道消息体长度就可以准备 空间了。准备好空间后,可以按照固定长度的接收来进行。具体看客户端代码。

客户端

#define _WINSOCK_DEPRECATED_NO_WARNINGS // 为了使用inet_ntoa时不出现警告
#include <cstdio>
#include <Winsock2.h>
#pragma comment(lib, "ws2_32.lib")

#define BUF_LEN 250


int main()
{
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	char szMsg[] = "你好,服务器,我已经收到你的信息";

	wVersionRequested = MAKEWORD(2, 2); //初始化Winsock库

	err = WSAStartup(wVersionRequested, &wsaData);
	if (err != 0) return 0;

	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) //判断返回的版本号是否正确
	{
		WSACleanup();
		return 0;
	}
	SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);//新建一个套接字

	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //服务器的IP
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(8000); //服务器的监听端口
	err = connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); //向服务器发出连接请求
	if (SOCKET_ERROR == err) //判断连接是否成功
	{
		printf("连接服务器失败,请检查服务器是否启动\n");
		return 0;
	}

	char recvBuf[BUF_LEN];
	int i, cn = 1, iRes;

	int leftlen;
	unsigned char* pdata;

	iRes = recv(sockClient, (char*)&leftlen, sizeof(int), 0); //接收来自服务器的信息

	leftlen = ntohl(leftlen);

	while (leftlen > 0)
	{
		iRes = recv(sockClient, recvBuf, BUF_LEN, 0); //接收来自服务器的信息
		if (iRes > 0)
		{
			printf("\nNo.%d:Recv %d bytes:", cn++, iRes);
			for (i = 0; i < iRes; i++)
				printf("%c", recvBuf[i]);
			printf("\n");
		}
		else if (iRes == 0)//对方关闭连接
			puts("\n服务端关闭发送连接了。。。\n");
		else
		{
			printf("recv failed:%d\n", WSAGetLastError());
			printf("recv failed with error: %d\n", WSAGetLastError());
			closesocket(sockClient);
			WSACleanup();
			return 1;
		}
		leftlen = leftlen - iRes;
	}



	char sendBuf[100];
	sprintf_s(sendBuf, "我是客户端,我已经完成数据接收了");//组成字符串
	send(sockClient, sendBuf, strlen(sendBuf) + 1, 0); //发送字符串给客户端
	memset(sendBuf, 0, sizeof(sendBuf));

	puts("向服务端发送数据完成");
	closesocket(sockClient); //关闭套接字
	WSACleanup(); //释放套接字库
	system(0);


	return 0;
}

代码和定长接收的例子的客户端类似,只不过多了先接收4字节的消息体长度值,然后分 配这个大小空间,后面的接收又和定长接收一样了。
有一点要注意,从recv 函数接收下来的长度要转为主机字节序:
在这里插入图片描述

这是因为服务器端程序是把长度转为网络字节序后再发送出去的。有些人可能会觉得这样 做多此一举,因为双方不转似乎也能得到正确长度。这是因为这些人是在本机或局域网环境下 测试的,并没有经过路由器网络环境。大家最好保持转的习惯,因为路由器和路由器之间都是 按网络字节序转发的。大家在编写网络程序时碰到发送整型时,应该转为网络字节序再发送, 接收时转为主机字节序再使用。

在这里插入图片描述

收了22次的250字节和最后一次的50字节,加起来正好是5550字节数据。

七、I/O 控制命令

套接字的 I/O 控制主要用于设置套接字的工作模式(阻塞模式还是非阻塞模式)。另外, 也可以用来获取与套接字相关的I/O 操作的参数信息。
Winsock提供了函数ioctlsocket 和 WSAloctl 来发送I/O 控制命令,前者源自Winsock1 版 本,后者是前者的扩展版本,源自Winsock2 版本。函数ioctlsocket声明如下:

其 中 ,s 为要设置I/O 模式的套接字的描述符。cmd 表示发给套接字的I/O 控制命令,通 常取值如下:

  • FIONBIO: 表示设置或清除阻塞模式的命令,当argp 作为输入参数为0的时候,套 接字将设置为阻塞模式;当argp 作为输入参数为非0的时候,套接字将设置为非阻 塞模式。有一种情况要注意:函数WSAAsynSelect 会将套接字自动设置为非阻塞模 式,而且如果对某个套接字调用了WSAAsynSelect 函数,再想用ioctlsocket 函数把 套接字重新设置为阻塞模式,ioctlsocket 会返回WSAEINVAL 错误,此时如果想把 套接字重新设置为阻塞模式,应该依旧调用WSAAsynSelect 函数,并把其参数IEvent 设置为0,这样套接字就又可变为阻塞模式了。大家今后在使用WSAAsynSelect 函 数的时候要做到心中有数,别想当然地以为通过ioctlsocket 函数一定能把套接字设为 阻塞模式。
  • FIONREAD: 用于确定套接字 s 自动读入数据量的命令,若 s 是流套接字 (SOCET STREAM) 类型,则argp 得到函数recv 调用一次时可读入的数据量,通 常和套接字中排队的数据总量相同;若s 是数据报套接字(SOCK DGRAM), 则 argp 返回套接字排队的第一个数据报的大小。
  • FIOASYNC: 表示设置或清除异步I/O 的命令。

argp 为命令参数,是一个输入输出参数。如果函数成功就返回零,否则返回 SOCKET ERROR,此时可以用函数WSAGetLastError 获取错误码。

比如下面的代码设置套接字为阻塞模式:
在这里插入图片描述
如果参数iMode 传入的是0,就设置阻塞,否则设置为非阻塞。

函 数WSAloctl 是 Winsock2 中 的I/O 控制命令函数,功能更为强大,增加了一些输入参数, 添加了一些新选项,并增加了一些输出函数以获得更多的信息,函数声明如下:
在这里插入图片描述

  • s:[in] 套接字描述符(句柄)。
  • dwIoControlCode:[in]存放用于操作的控制码,比如 SIO_RCVALL (接收全部数据 包的选项)。
  • lpvInBuffer:[in]指向输入缓冲区地址。
  • cbInBuffer:[in]输入缓冲区的字节大小。
  • lpvOutBuffer:[out]指向输出缓冲区地址。
  • cbOutBuffer:[in] 输出缓冲区的字节大小。
  • lpcbBytesReturned:[out] 指向存放实际输出数据的字节大小的变量地址。
  • lpOverlapped:[in]指向WSAOVERLAPPE D 结构体的地址(若是非重叠套接字则忽 略该参数)。
  • lpCompletionRoutine:[in] 指向一个例程函数,该函数会在操作结束后调用(若是非 重叠套接字则忽略该参数)。

如果函数成功就返回0,否则返回SOCKET ERROR, 可 用WSAGetLastError 获取错误码。


设置阻塞套接字为非阻塞套接字

#define _WINSOCK_DEPRECATED_NO_WARNINGS // 为了使用inet_ntoa时不出现警告
#include <Winsock2.h>
#pragma comment(lib, "ws2_32.lib") //Winsock库的引入库
#include <assert.h>
#include <stdio.h>


int main(int argc, char* argv[])
{
	u_long argp;
	int res;
	char ip[] = "120.4.6.99"; //120.4.6.99是和本机通一网段的地址 ,但并不存在  
	int port = 13334;
	struct sockaddr_in server_address;

	// Initialize Winsock
	WSADATA wsaData;
	int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != NO_ERROR)
		printf("Error at WSAStartup()\n");




	memset(&server_address, 0, sizeof(server_address));
	server_address.sin_family = AF_INET;
	DWORD dwIP = inet_addr(ip);
	server_address.sin_addr.s_addr = dwIP;
	server_address.sin_port = htons(port);

	SOCKET sock = socket(PF_INET, SOCK_STREAM, 0);
	assert(sock >= 0);


	long t1 = GetTickCount();

	int ret = connect(sock, (struct sockaddr*)&server_address, sizeof(server_address));
	printf("connect ret code is: %d\n", ret);
	if (ret == -1)
	{

		long t2 = GetTickCount();

		printf("time used:%dms\n", t2 - t1);

		printf("connect failed...\n");
		if (errno == EINPROGRESS)
		{
			printf("unblock mode ret code...\n");
		}
	}
	else
	{
		printf("ret code is: %d\n", ret);
	}



	argp = 1;
	res = ioctlsocket(sock, FIONBIO, (u_long FAR*) & argp); //设置套接字为非阻塞模式
	if (SOCKET_ERROR == res)
	{
		printf("Error at ioctlsocket(): %ld\n", WSAGetLastError());
		WSACleanup();
		return -1;
	}


	puts("设置非阻塞模式后:\n");

	memset(&server_address, 0, sizeof(server_address));
	server_address.sin_family = AF_INET;
	dwIP = inet_addr(ip);
	server_address.sin_addr.s_addr = dwIP;
	server_address.sin_port = htons(port);




	t1 = GetTickCount();

	ret = connect(sock, (struct sockaddr*)&server_address, sizeof(server_address));
	printf("connect ret code is: %d\n", ret);
	if (ret == -1)
	{

		long t2 = GetTickCount();

		printf("time used:%dms\n", t2 - t1);

		printf("connect failed...\n");
		if (errno == EINPROGRESS)
		{
			printf("unblock mode ret code...\n");
		}
	}
	else
	{
		printf("ret code is: %d\n", ret);
	}



	closesocket(sock);
	WSACleanup(); //释放套接字库


	return 0;
}

在代码中,我们首先创建了一个套接字 sock, 刚开始默认是阻塞的,然后用connect 函数 去连接一个和本机IP 同一子网的不真实存在的IP,会发现用了20多秒。接着我们用ioctlsocket 函数把套接字sock 设置为非阻塞,再同样用connect 函数去连接一个和本机IP 同一子网的不 真实存在的IP, 会发现connect 立即返回了,这就说明我们设置套接字为非阻塞成功了。

在这里插入图片描述

可以看到,大概等了20多秒后才提示 connect 失败。

需要注意的是,当套接字处于阻塞模式时,该函数可能阻塞线程;若套接字处于非阻塞模 式且指定的操作不能及时完成时,WSAGetLastError 将返回WSAEWOULDBLOCK 错误码, 此时程序可以将套接字改为阻塞模式后再次发送请求。

八 、套接字选项

8.1 基本概念

除了可以通过发送I/O 控制命令来影响套接字的行为外,还可以设置套接字的选项来进 一步对套接字进行控制,比如我们可以设置套接字的接收或发送缓冲区大小、指定是否允许套 接字绑定到一个已经使用的地址、判断套接字是否支持广播、控制带外数据的处理、获取和设 置超时参数等。当然除了设置选项外,还可以获取选项,选项的概念相当于属性的意思。所以 套接字选项也可说是套接字属性,选项就是用来描述套接字本身属性特征的。

值得注意的是,有些选项(属性)只可获取,不可设置,而有些选项既可设置也可获取。

8.2 选项的级别

有一些选项是针对一种特定协议的,意思就是这些选项都是某种套接字特有的;又有一些 选项适用于所有类型的套接字,因此就有了选项级别(level) 概念,即选项的适用范围或适用 对象,是适用所有类型套接字还是适用某种类型套接字。常用的级别有:

  • SOL_SOCKET: 该级别的选项与套接字使用的具体协议无关,只作用于套接字本身。
  • SOL_LRLMP: 该级别的选项作用于IrDA 协议。
  • IPPROTO_IP:该级别的选项作用于IPv4 协议,因此与IPv4 协议的属性密切相关, 比如获取和设置IPv4 头部的特定字段。
  • IPPROTO_IPV6:该级别的选项作用于IPv6 协议,有一些选项和IPPROTO_IP 对应。
  • IPPROTO_RM: 该级别的选项作用于可靠的多播传输。
  • IPPROTO TCP: 该级别的选项适用于流式套接字。
  • IPPROTO UDP:该级别的选项适用于数据报套接字。 这些都是宏定义,可以直接用在函数参数中。

通常,不同的级别选项值也不尽相同。下面我们来看一下级别为SOL_SOCKET 的选项(见
表5- 3)。
在这里插入图片描述
在这里插入图片描述

再来看一下级别IPPROTO IP 的常用选项(见表5-4)。

在这里插入图片描述

8.3 获取套接字选项

Winsock 提 供 了API 函 数getsockopt 来获取套接字的选项。函数getsockopt 声明如下:
在这里插入图片描述
其中,参数s 是套接字描述符;level 表示选项的级别,比如可以取值 SOL SOCKET、
IPPROTO IP 、IPPROTO TCP 、IPPROTO UDP 等 ;optname 表示要获取的选项名称;optval[out] 指向存放接收到的选项内容的缓冲区,char*表示传入的是optval的 地 址 ,optval具体类型要根 据选项而定,具体可以参考5.8.2小节;optlen[in,out] 指 向optval 所指缓冲区的大小。如果函数执行成功就返回0,否则返回SOCKET ERROR, 此时可用函数WSAGetLastError 来获得错误码,常见的错误码如下:

  • WSANOTINITIALISED:在调用getsockopt 函数前没有成功调用WSAStartup 函数
  • WSAENETDOWN: 网络子系统出现故障。
  • WSAEFAULT: 参 数optlen 太小或optval 所指缓冲区非法。
  • WSAEINPROGRESS:一个阻塞的Windows Sockets 1.1调用正在进行,或WindowsSockets 在处理一个回调函数
  • WSAEINVAL: 参 数level 未知或非法。
  • WSAENOPROTOOPT: 选项未知或不被指定的协议簇所支持。
  • WSAENOTSOCK: 描述符不是一个套接字描述符。

获取流和数据报套接字接收和发送的(内核)缓冲区大小

#define _WINSOCK_DEPRECATED_NO_WARNINGS // 为了使用inet_ntoa时不出现警告

#include <stdio.h>
#include <Winsock2.h>
#pragma comment(lib, "ws2_32.lib") //Winsock库的引入库

int main()
{
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;

	wVersionRequested = MAKEWORD(2, 2); //制作Winsock库的版本号
	err = WSAStartup(wVersionRequested, &wsaData); //初始化Winsock库
	if (err != 0) return 0;


	//---------------------------------------
	// 创建侦听套接字
	SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //流式套接字、使用TCP协议
	if (s == INVALID_SOCKET) {
		printf("Error at socket()\n");
		WSACleanup();
		return -1;
	}

	SOCKET su = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //数据报套接字、使用UDP协议
	if (s == INVALID_SOCKET) {
		printf("Error at socket()\n");
		WSACleanup();
		return -1;
	}


	DWORD optVal;
	int optLen = sizeof(optVal);

	if (getsockopt(s, SOL_SOCKET, SO_RCVBUF, (char*)&optVal, &optLen) == SOCKET_ERROR)
		printf("getsockopt failed:%d", WSAGetLastError());
	else
		printf("流套接字接收缓冲区的大小: %ld bytes\n", optVal);

	if (getsockopt(s, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen) == SOCKET_ERROR)
		printf("getsockopt failed:%d", WSAGetLastError());
	else
		printf("流套接字发送缓冲区的大小: %ld bytes\n", optVal);


	if (getsockopt(su, SOL_SOCKET, SO_RCVBUF, (char*)&optVal, &optLen) == SOCKET_ERROR)
		printf("getsockopt failed:%d", WSAGetLastError());
	else
		printf("数据报套接字接收缓冲区的大小: %ld bytes\n", optVal);

	if (getsockopt(su, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen) == SOCKET_ERROR)
		printf("getsockopt failed:%d", WSAGetLastError());
	else
		printf("数据报套接字发送缓冲区的大小: %ld bytes\n", optVal);

	WSACleanup();
	return 0;
}

在上述代码中,首先创建了一个流套接字和数据报套接字,然后通过getsockopt 函数来获 取它们接收和发送缓冲区的大小,最后输出。注意,缓冲区大小的选项级别是SOL_SOCKET, 不要写错了。而且,获取缓冲区大小的时候,optVal的类型要定义为DWORD, 然后把其指针 传给getsockopt。

在这里插入图片描述


获取当前套接字类型

#define _WINSOCK_DEPRECATED_NO_WARNINGS // 为了使用inet_ntoa时不出现警告

#include <Winsock2.h>
#include<iostream>
#pragma comment(lib, "ws2_32.lib") //Winsock库的引入库

int main(){
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;

	wVersionRequested = MAKEWORD(2, 2); //制作Winsock库的版本号
	err = WSAStartup(wVersionRequested, &wsaData); //初始化Winsock库
	if (err != 0) return 0;

	SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (s == INVALID_SOCKET) {
		printf("Error at socket()\n");
		WSACleanup();
		return -1;
	}

	SOCKET su = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (s == INVALID_SOCKET) {
		printf("Error at socket()\n");
		WSACleanup();
		return -1;
	}

	DWORD optVal;
	int optLen = sizeof(optVal);

	if (getsockopt(s, SOL_SOCKET, SO_TYPE, (char*)&optVal, &optLen) == SOCKET_ERROR)
		printf("getsockopt failed:%d", WSAGetLastError());
	else
	{
		if (optVal == SOCK_STREAM)
			printf("当前套接字是流套接字\n");
		else if (SOCK_DGRAM == optVal)
			printf("当前套接字是数据报套接字\n");
	}
	if (getsockopt(su, SOL_SOCKET, SO_TYPE, (char*)&optVal, &optLen) == SOCKET_ERROR)
		printf("getsockopt failed:%d", WSAGetLastError());
	else
	{
		if (optVal == SOCK_STREAM)
			printf("当前套接字是流套接字\n");
		else if (SOCK_DGRAM == optVal)
			printf("当前套接字是数据报套接字\n");
	}


	WSACleanup();
	return 0;

}


在上述代码中,先创建了一个流套接字s 和数据报套接字su, 然后用getsockopt 来获取套接字类型并输出。获取套接字类型的选项是SO_TYPE, 因此我们把SO_TYPE 传 入getsockopt
函数中。

在这里插入图片描述


判断套接字是否处于监听状态

#define _WINSOCK_DEPRECATED_NO_WARNINGS // 为了使用inet_ntoa时不出现警告
#include <Winsock2.h>
#include<iostream>

#pragma comment(lib, "ws2_32.lib") //Winsock库的引入库

int main()
{
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	sockaddr_in service;
	char ip[] = "127.0.0.1";//本机ip

	wVersionRequested = MAKEWORD(2, 2); //制作Winsock库的版本号
	err = WSAStartup(wVersionRequested, &wsaData); //初始化Winsock库
	if (err != 0) return 0;

	SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (s == INVALID_SOCKET) {
		printf("Error at socket()\n");
		WSACleanup();
		return -1;
	}

	service.sin_family = AF_INET;
	service.sin_addr.s_addr = inet_addr(ip);
	service.sin_port = htons(9900);
	if (bind(s, (SOCKADDR*)&service, sizeof(service)) == SOCKET_ERROR)
	{
		printf("bind failed\n");
		WSACleanup();
		return -1;
	}


	DWORD optVal;
	int optLen = sizeof(optVal);

	if (getsockopt(s, SOL_SOCKET, SO_ACCEPTCONN, (char*)&optVal, &optLen) == SOCKET_ERROR)
		printf("getsockopt failed:%d", WSAGetLastError());
	else printf("监听前,选项SO_ACCEPTCONN的值=%ld,套接字未处于监听状态\n", optVal);

	// 开始侦听
	if (listen(s, 100) == SOCKET_ERROR)
	{
		printf("listen failed:%d\n", WSAGetLastError());
		WSACleanup();
		return -1;
	}

	if (getsockopt(s, SOL_SOCKET, SO_ACCEPTCONN, (char*)&optVal, &optLen) == SOCKET_ERROR)
	{
		printf("getsockopt failed:%d", WSAGetLastError());
		WSACleanup();
		return -1;
	}
	else printf("监听后,选项SO_ACCEPTCONN的值=%ld,套接字处于监听状态\n", optVal);

	WSACleanup();

	return 0;
}


在上述代码中,分别在调用监听函数listen 前后分别获取了选项SO_ACCEPTCONN 的值,
可以发现监听前该选项值为0,监听后选项值为1了,符合预期。
在这里插入图片描述

8.4 设置套接字选项

Winsock提供了API 函数setsockopt 来获取套接字的选项。函数getsockopt 声明如下:
在这里插入图片描述
其中,参数s 是套接字描述符;level 表示选项的级别,比如可以取值 SOL SOCKET、
IPPROTO_IP 、IPPROTO_TCP 、IPPROTO_UDP 等 ;

optname 表示要获取的选项名称;optval 指向存放要设置的选项值的缓冲区,char*表示传入的是optval的 地 址 ,optval具体类型要根据 选项而定,具体可以参考5.8.2小节的内容;optlen 指 向optval 所指缓冲区的大小。如果函数 执行成功就返回0,否则返回SOCKET_ERROR,此时可用函数WSAGetLastError 来获得错误 码。错误码和getsockopt 出错时类似,这里不再赘述。


启用套接字的保活机制

#define _WINSOCK_DEPRECATED_NO_WARNINGS // 为了使用inet_ntoa时不出现警告

#include<iostream>
#include <Winsock2.h>
#pragma comment(lib, "ws2_32.lib") //Winsock库的引入库

int main()
{
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	sockaddr_in service;
	char ip[] = "127.0.0.1";//本机ip

	wVersionRequested = MAKEWORD(2, 2); //制作Winsock库的版本号
	err = WSAStartup(wVersionRequested, &wsaData); //初始化Winsock库
	if (err != 0) return 0;

	SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建一个流套接字
	if (s == INVALID_SOCKET) {
		printf("Error at socket()\n");
		WSACleanup();
		return -1;
	}

	service.sin_family = AF_INET;
	service.sin_addr.s_addr = inet_addr(ip);
	service.sin_port = htons(9900);
	if (bind(s, (SOCKADDR*)&service, sizeof(service)) == SOCKET_ERROR) //绑定套接字
	{
		printf("bind failed\n");
		WSACleanup();
		return -1;
	}


	BOOL  optVal = TRUE;//一定要初始化
	int optLen = sizeof(BOOL);

	//获取选项SO_KEEPALIVE的值
	if (getsockopt(s, SOL_SOCKET, SO_KEEPALIVE, (char*)&optVal, &optLen) == SOCKET_ERROR)
	{
		printf("getsockopt failed:%d", WSAGetLastError());
		WSACleanup();
		return -1;
	}
	else printf("监听后,选项SO_ACCEPTCONN的值=%ld\n", optVal);

	optVal = TRUE;
	if (setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, (char*)&optVal, optLen) != SOCKET_ERROR)
	{
		printf("启用保活机制成功\n");
	}
	if (getsockopt(s, SOL_SOCKET, SO_KEEPALIVE, (char*)&optVal, &optLen) == SOCKET_ERROR)
	{
		printf("getsockopt failed:%d", WSAGetLastError());
		WSACleanup();
		return -1;
	}
	else printf("设置后,选项SO_KEEPALIVE的值=%d\n", optVal);

	WSACleanup();
	system("pause");
	return 0;
}

在这里插入图片描述

值得注意的是,存放选项SO KEEPALIVE值的变量类型是BOOL, 并且要初始化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值