[笔记] Microsoft Windows网络编程《一》WinSock简介

文章目录

  • 前言
  • 1.1 WinSock头文件及库文件
  • 1.2 WinSock的初始化
  • 1.3 错误检查和处理
  • 1.4 协议寻址
    • 1.4.1 字节排序
  • 1.5 创建套接字
  • 1.6 面向连接的通信
    • 1.6.1 服务器API函数
      • 1.6.1.1 绑定
      • 1.6.1.2 监听
      • 1.6.1.3 接受连接
    • 1.6.2 客户端API函数
      • TCP 状态
      • connect 函数
    • 1.6.3 数据传输
      • 1.6.3.1 send 和 WSASend
      • 1.6.3.2 WSASendDisconnect
        • 带外数据
      • 1.6.3.3 recv和WSARecv
      • 1.6.3.4 WSARecvDisconnect
    • 1.6.4 流协议
      • 散播一聚集1/0
    • 1.6.5 中断连接
      • 1.6.5.1 shutdown
      • 1.6.5.2 closesocket
  • 1.7 无连接通信
    • 1.7.1 接收端
    • 1.7.2 发送端
    • 1.7.3 基于消息的协议
    • 1.7.4 释放套接字资源
  • 1.8 其他API函数
    • 1.8.1 getpeername
    • 1.8.2 getsockname
    • 1.8.3 WSADuplicateSocket
  • 1.9 Windows CE
  • 总结


前言

本章专门讲解编写成功的 Winsock 用程序的基木方法。

Winsock 是一种标准 API(Application Programming Interface,应用程序编程接口),要用于网络中的数据通信,它允许两个或者多个应开程序(或进程)在同一台机器上或通过网络相互通信。

有点我们必须明白:

  • Winsock 是一种网络编程接口,而不是协议

使用 Winsock 编程接口,应用程序可通过通网络协议如 TCP/IP(TransmissionControl Protocol/Internet Protocol,传输控制协议/网际协议) 或 IPX(Intermet Packet Exchange, Internet 数据包交换)协议建立通信。

Winsock 接口从在UNX 平台上实现的 BSD Socket(套接字中继承了大量的特性。在 Windows 坏境中,这种接口演变成一种真正独立于协议的接口,新发布的 Winsock2版本更是如此。

本章将讨论 从网络上的一台机器到另一台机器建立通信的基本知识,以及如何收发数据

为了便于大家理解接受连接、建立连接和收发数据所需的 Winsock 调用,本章给出了多个示例。

由于本章的目的是学习这些基本的 Winsock 调用,因而所举的示例均采用了直接阻塞的 Winsock 调用。

第5章将讲述 Winsock 支持的非阻塞调用及其他各种 I/O 方法,其中包含示例代码。

除此以外,本章还将介绍各种 API函数的:

  • Winsock 1版本
  • Winsock 2版本。

通过前 WSA 可以区分该函数的两种版本,若 Winsock 2 在其规范中更新或增添了一个新的 API 函数,该函数名将带有WSA前缀。比如,建立套接字的 Winsock1函数只是被简单称为socket。而Winsock2引入该函数的新版本时,则将它命名为 WSASocket,该函数可以使用 Winsock2中出现的一些新特性。

但请注意这个命名规则有几个例外,如WSAStartupWSACleanupWSARecvExWSAGetLastError 都属于Winsock 1.1规范的函数

在使用 Winsock 开发应用程序前,必须了解创建应用程序时学要哪些文件和库。


1.1 WinSock头文件及库文件

如前所述,Winsock 有两个主要版本,

  • Winsock1
  • Winsock2

两者都能在除 Windows CE之外(Windows CE 只支持 Winsock 1)的所有 Windows 平台上运行。

开发新的应用程序时,把WINSOCK2.H 文件包含在应用程序中,该程序将使用 Winsock2 规范。为了和其他旧的 Winsock 应用程序兼容以及保证 Windows CE 平台上的程序开发,可以使用 WINSOCKH。

另外,还有个头文件MSWSOCKH,该头文件用于微软专用编程扩展,这些扩展通常用于高效 Winsock 应用程序的开发第6章中将对此加以描述在编译采用广WINSOCK2.H 的应用程序时,须链接 WS2_32.LIB 库。

使用 WINSOCKH比如在Windows CE 中)时须使用 WSOCK32LIB。如果从 MSWSOCKH 中使用扩展 API,还必须链接MSWSOCKDLL。一旦包含了必需的头文件和链接环境,就以开始编写应用程序代码了,这时需要初始化WinSock。


1.2 WinSock的初始化

每个Wimsock 应用都必须加载合适的 Winsock DLL版本。如调用·个Winsock 所数之前没有加载 Winsock 库,这个函数就会返回一个 SOCKET ERROR,错误信息是 WSANOTINITIALISED加载 Winsock 库是通过调用 WSAStartup 函数实现的。这个函数的定义如下:

int WSAStartup(
WORD wVersionRequested,
LPWSADATA lpwsAData
);

wVersionRequested参数用于指定准备加载的 Winsock 库的版本。高位字节指定所需 Winsock 库的次版本,而低位字节则是主版本。可以使用宏 MAKEWORD(x,yX其中,x是高位字节,y 是低位字节)来方便地获得 wVersionRequested 的正确值。

IpWSAData参数是指向 LPWSADATA 结构的指针WSAStartup 用与其加载的库版本有关的信息填充这个结构:

typedef struct WSAData
{
	WORD wVersion
	WORD wHighVersion
	char szDescription[WSADESCRIPTION LEN +1]:
	char szSystemstatus[WSASYS STATUS LEN +1];
	unsigned short iMaxSockets;
	uns igned short iMaxUdpDq;
	char FAR* lpVendorInfo;
	WSADATA,ALPWSADATA;
}

WSAStartup 将第一个字段 wVersion 设置为将要使用的 Winsock 版本。
wHighVersion 参数包含了现有 Winsock 库的最高版本。

记住,这两个字段中,高位字节代表的是 Winsock 次版本,而低位字节代衣的则是 Winsock 主版本,szDescriptionszSystemStatus 这两个宁段由特定的Winsock 来实现和设置,它们并没有实际作用。

不要使用下面这两个字段:

  • iMaxSockets
  • iMaxUdpDg

它们分别表示可以时打开的取大套接字数量,以及数报的最大长度。

通过 WSAEnumProtocols(请参阅第 2章查询协议信息,才能知道数据报的最人长度:可以同时打开的最大套接字数量个是固定的,它的值在很人程度上取决于可用的物理资源。最后的 pVendorlnfo是 个保留子段,用下存储实现 Winsock的特定供应商的信息,所有的 Windows 平台都没有使用这个学段。

1.1 列出了微软各种 Windows 平台文持的 Winsock 版本。必须注意这两个版本之间的差别Winsock l.x 不支持描述的很多高级Winsock 特性

[笔记] Microsoft Windows网络编程《一》WinSock简介_第1张图片
注意,即使某个平台支持 Winsock 2,也可以不使用这个 Winsock 的最新版本。也就是说,如果打算编写大多数平台均能支持的应用程序,应根据 Winsock 1.1 规范来编写。为所有的 Winsock 1.1调用都通过 Winsock 2 DLL 映射,所以所编程序可以在Windows NT 4.0 平台上确无误地运行同时,如果市面上出现了您所用平台可以使用的 Winsock 库更新版本,那么您很可能要进行版本升级。内为这些新版本中仙含了对错误的修止,所以从理论上来说,旧代码完全可以正常运行。

在有些情况下,Wimsock 堆栈的行为和规范中的定义有所不同,这样,许多程序员是根据特定!标平台的行为来编程,前不是根据规范来编写程序。

但是,在很多情况下,编写新应用程序时,程序员都想加载已发布的 Winsock 库的最新版本。当然,如果 Winsock 3 发布了,加载 2.2 版本的应用程序仍可一如既社地运行。如果用户要求的 Winsock版本比平台所能支持的版本新,WSAStartup 就会失败。

如果WSAStartup 正确返回,WSADATA 结构中的wHighVersion 就是当前系统中的 Windsock 库能够支持的最新版本。
在使用Winsock 接口编写好应用程序之后,应该调用WSACleanup函数这个函数能够使 Winsock释放所有由 Winsock分配的资源,并取消这个应用程序挂起的 Winsock 调用。

WSACleanup数的定义为:

int WSACleanup(void);

因为操作系统将会自动释放资源,所以退出应用程序时也可以不调用 WSACleanup 函数:

然而如果这样做,您的应用程序就不再符合 Winsock 规范了。另外,每次调用 WSAStartup 后都应该调用WSACleanup。

1.3 错误检查和处理

要想成功编写 Winsock 应用程序,检查和处理错误是全关重要的,所以这里首先对此进行介绍事实上,对 Winsock 函数来说,返回错误是很常见的,但是,在有些情况下,这些错误是无关紧要的通信仍可在那个套接子上进行。Winsock 调用失败时最常见的返回值是 SOCKET ERROR。本书在详细介绍各个API调用时,都将省出和各种错误对应的返回值。

SOCKET ERROR 常量实际上是-1。

如果调用 Winsock 函数时出现了错误,可以用 WSAGetLastError 函数来获得一段代码,这段代码专用来说明错误。该函数的定义如下:

int WSAGetLastError(void);

发生错误之后调用这个函数,返回的是所发生的错误的整数代码。

WSAGetLastError 函数返回的这些错误代码都有已经预先定义的常量值;

因 Winsock 版本的不同,这些值的声明可能在WINSOCK1.H中,也可能在 WINSOCK2 中。

两个头文件的惟一差别是:

  • WINSOCK2H 中包含更多针对 Winsock2中引入的一些新API函数和功能的错误代码。
  • 为各种错误代码定义的常量(带有#dfinc指令)一般都以 WSAE 开头。

相时于 WSAGetLastError函数,另一个与此关的函数是WSASetLastError,使用该函数可以手动设置WSAGetLastError 获取的错误代码。

下述程序演示了如何基于上述内容构建一个Winsock 应用程序框架:

#includecwinsock2.h>
void main(void)
{
WSADATA wsaDatd;
//初始化winsock版本2.2
if ((Ret = WSAStartup(MAKEWORD(2,2),EwsaData))!= 0)
//注意:因为winsock没有加载,所以我们不能使用WSAGetLastError来确定导致故障的特定错//误。但我们将根据WSAStartup的返回状态进行判断
	prinTf("WSAStartup failed with error ed n",Ret);return;
//当应用程序结束调用WSACleanup之片,设置Winsock通信代码
if (wSACleanup() == SOCKET ERROR)
	printf("wSACleanup falled with error d n",WSAGetLastError());
}

下面开始叙述如何使用网络协议建立通信。

1.4 协议寻址

随着Internet 的不断普及,IP 协议随处可得,当今大多数 Winsock 应用程序开发都使用IP,因此为了简便和避免重复,本章其余部分将只描述怎样使用IP(Internet Protocol,网际协议)协议创建基本的Winsock 调用来建立通信。前面已提到,Winsock 是一种独立于协议的接口,第4章将讲述使用其他协议(如 IPX)的具体内容。另外,本章对 IP 的讨论仅限于简单描述 IPV4(IP 第4版)第3章将全面而详细地讲述所有IP版本,包括IPv4 和IPV6(IP 第6版)

本章的其余部分将介绍使用 IPy4 协议建立 Winsock 通信的基本知识。IP 在大多数计算机操作系统中都能得到支持,并可在多数局域网(LAN)上使用,如办公室中的小型网络,也可在广域网(WAN)中使用,例如 Intemmet。从设计角度看,IP 是种无连接协议,它不能确保数据传输的成功。两个高级协议-TCP(Transmission Control Protocol,输控制协议)和 UDP(User Datagram Protocol,用户数据报协议)用通过 IP 进行面向连接和无连接的数据通信,这方面的内容将在以后讲述。TCP 和UDP 都使用IP 进行数据传输,通常被称作 TCP/IP 利UDP/IP。如果需要在 Winsock 中使用IPy4,需要知道怎样为IPv4 寻址。

在IPv4 中,计算机都分配有一个地址,该地址用一个 32 位的数值来表示。客户机需要通过 TCP或UDP 和服务器通信时,必须指定服务器的 IP 地址和服务端号。另外,服务器打算监听代入的客户机请求时,也必须指定一个IP地址和一个端口号。

在Winsock 中,应用程序通过SOCKADDR IN结构来指定 IP 地址和服务端口信息,该结构的格式如下:

struct sockaddr_in{
	short sin family;
	u_short sin port;
	sin addr struct in addr;
	charsin_zero[8];
}

sin family 字段必须设为AF_INET ,以告知 Winsock 此时正在使用IP 地址族。

用标识服务器服务的 TCP 或 UDP 通信端口 sin port 字定义。因为有些可用端口号是为“过知的"服务保留的,如 FTP(文件传输协议,File Transfer Protocol) 和 HTTP(Hypertext Transfer Protocol.超文本传输协议),所以应用程序在选择端口时,必须特别小心。第 2 章中有更多关于选择端的详细内容。

SOCKADDRIN结构的sin addr 宁段把IP4 地址作为一个4字节的量存储起来,它是无符号长整数的数据类型。

根据这个字段的不同用法,它还可表示一个本地或远程 IP 地址。IP 地址一般是用“Internet 标准点分表示法”像 a.b.c.d 一样指定的,其中每个字母代表一个宁节的数字(用十进制、八进制或十六进制格式表示),从无到右分配了一个无符号长整数的 4个字节。后-·个字段 si zero只充当填充项,以使SOCKADDR IN 结构和 SOCKADDR 结构的长度一样。
inet_addr是一个很实用的支持函数,它可把一个点分IP地址转换成一个32 位的无符号长整数

它的定义如下:

unsigned long inet_addr (const char FAR *cp);

cp 字段是一个空终止字符串,用于接受点分表示法的 P 地址。注意,这个函数把IP 地址当作-个按网络字节顺序排列的 32 位无符号长整数返回(网络字节顺序在下面的“字节排序”小节中有简费说明)。

1.4.1 字节排序

不同的计算机处理器采用 big-endian 和 little-endian 形式进行编号,具体采用哪种表示方法,由各自的设计决定。比如,Intel86 处理器上,多字节编号用 little-endian 形式来表示:

  • 字节的排序是从最无意义的字节到最有意义的字节。

在计算机中把 IP 地址和端口号指定成多字节数时,这个数就按 主机字节(host-byte)顺序 来表示。

但是,如果在网络上指定P 地址和端口号,“Intemet 联网标准”指定多字节值必须用 big-endian 形式来表示(从最有意义的宁节到最无意义的字节),般称之为 网络字节(network-byte)顺序

有一系列函数可用于多字节数的转换,把后者从主机字节顺序转换成网络字节顺序,或进行反方向的转换。

下面 4 个API函数使将一个数从机宁节顺序转换成网络字节顺序:

u_long htonl(u long hostlorg);

int WSAHtonl(
SOCKET s,
u_long hostlong,
u_long FAR * lpnetlong
);

u_short htons( short hostshort);

int WSAHtons(
SOCKET s,
u_short hostshort,
u_short FAR * lpnetshort
)

htonlWSAHtonlhostlong 参数是按主机子节顺序排序的一个 4字节数。

htonl函数返回的数是网络字节顺序,而 WSAHtonl 函数通过 pnetlong 参数返的数也按网络节顺序排列。

htons 和WSAHtons 的 hostshort 参数是按机字节顺序排列的一个2节数。

htons 函数把这个数当作按网络字节顺序排列的一个2字节数值返回,而WSAHtons 函数则通过lpnetshort 参数返这个数。

下面这 4 个函数是前面4 个函数的逆向数,它们把网络字节顺序转换成主机字节顺序。

u_long ntohl(u long netlong);

int WSANtohl(
SOCKET s,
u_long netlong,
u_long FAR * lphostlong,
u_short ntohs,
u_short netshort
);

int WSANtohs (
SOCKET s,
u_short netshort,
u_short FAR * lphostshort
)

现在,演示··下如何利用上面描述的 inet_addr 和 htons 函数来创建 SOCKADDR IN 结构,并进行IPv4 寻址。

SOCKADDR_IN InternetAddr;
INT nPortId = 5150;
InternetAddr.sin family = AF_INET;
//将准备使用的点分Internet 地址 136,149.329 转换为4字节整数,并把它分配给 sin addx
InternetAddr.sin addr.s addr = inet_addr("136,149,3,29");
//nPoxtId 变量按存储主机宁节顺序排列。将 nPortId 转换为网络字节顺序,并分配给 sin port
InternetAddr.sin port = htons(nPortid);

IP 地址不便于记忆,因此,大多数人更喜欢使用一个容易记忆且容易掌握的主机名。

第3 章将叙述一些有用的地址利名称解析两数,使用它们可以将主机名(如 www.somewebsite.com)解析为IP地址服务名称(如 FTP)或端口号。

这些函数有 getaddrinfo、getameinfo、gethostbyaddr、gethostbyname、gethostname、getprotobyname、getprotobynumber、getservbyname 以及 getservbyport 等。

同时还有这些函数的异步版本,如: WSAAsyncGetHostByAddr 、 WSAAsyncGetHostByNameWSAAsyncGetProtoByNameWSAAsyncGetProtoByNumber、WSAAsyncGetServByNameWSAAsyncGetServByPort 等。

现在有了协议寻址的基础,诸如 IPv4,就可以准备通过创建套接字来建立通信了。

1.5 创建套接字

熟悉 Winsock 的人应该知道,API 是建立在套接字概念基础上的。套接字是传输提供程序的句柄,在 Windows 中,套接字和文件描述符不是一回事,因而是一个独立的类型,即 WINSOCK2.H 中的SOCKET类型。

有两个函数可以用来创建套接字:

  • socket
  • WSASocket。

随后的3 章将详细讲述如何创建每种可用协议的套接字。

为简便起见,对套接字将仅作简要叙述:

SOCKET socket (
int af,
int type,
int protocol
)

第1个参数 af 是协议的地址族。由于本仅使用 IP4 来描述 Winsock,因此应将这个字段设为AF_INET

第 2个参数 type 是协议的套接字类型。如果使用 TCP/P 创建接字,应将该字段设为SOCK_STREAM,而用 UDP/IP 时则应设为 SOCK DGRAM。

第3 个参数是 protocol,用于在给定地址族和套接字类型具有多重入口时,对具体的传送作限定。对于 TCP,应将该字段设为 IPPROTO_TCP而对于UDP则设为IPPROTO_UDP第2章将详细讲述如何创建所有协议的接字,包括 WSASocketAPI在内。

为控制各种套接字选项和套接字行为,Winsock 提供了4个有用的函数:

  • setsockopt
  • getsockopt
  • ioctisocket
  • WSAloctl

在简单的 Wisock 编程中,没有必要特别使用这些函数。第7章节,将描述这些函数及其所有可用选项。

在成功地创建了套接字之后,就可以开始在套接字上建立通信,并为收发数据做好准备。

在 Winsock 中有两种基本的通信技术:

  • 面向连接的通信
  • 无连接的通信

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 展示了建立通信信道时,服务器和客户机必须执行的基本调用。

[笔记] Microsoft Windows网络编程《一》WinSock简介_第2张图片

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(22),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 步操作:

  1. 创建一个套接字。
  2. 建立·个 SOCKADDR 地址结构,结构名称为准备连接到的服务器名(以下层协议为准)。对于 TCP/IP,这是客户机应用程序所监听的服务器的 IP 地址和端口号。
  3. 用 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 包。应用程序调用 closesocketshutdown 时(把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状态。

[笔记] Microsoft Windows网络编程《一》WinSock简介_第3张图片
采取主动关闭措施时,沿两条路径可以进入 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(22),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();
}

既然已经能够为面向连接的服务器和客户机建立通信了,接下来便开始处理数据传输

1.6.3 数据传输

收发数据是网络编程的主题。要在已建立连接的套接字上发送数据,可用这两个 API函数:sed和 WSASend。第2个函数是 Winsock 2中特有的。同样地,在已建立了连接的套接字上接收数据也有两个函数:recv 和 WSARecV。后将也是 Winsock 2函数。必须牢记住这点: 所有关系到收发数据的缓冲区都属于简单的 char 类型,即面向字节的数据。事实上,它可能是一个包含任何原始数据的缓冲区,至]这个原始数据是…进制数据,还是字符型数据,则无关紧要。
另外,所有收发函数返回的错误代码都是 SOCKET ERROR。一旦有错误返回,系统就会调用WSAGetLastError 获得详细的错误信息。最常见的错误是WSAECONNABORTED和WSAECONNRESET。两者均涉及到连接正在被关闭这一问题–要么由于超时被关闭,要么由于通信方止在关闭连接。另··个常见错误是 WSAEWOULDBLOCK,·般出现在套接字处于非阻寒模式或只步状态时。这个错误意味着指定函数暂时不能完成。第5 将详细说明各种 Winock1/0 方法,以避免出现此类错误。

1.6.3.1 send 和 WSASend

要在已建立连接的套接子上发送数据,第1个可用的API 函数是 send,其原型为:

int send(SOCKET s, const char FAR * buf, int len,int flags);

SOCKET 参数是已建义了连接、将用于发送数据的套接子。第2个参数 buf则是指向学符缓冲区的指针,该缓冲区中包含即将发送的数据、第 3 个参数 e,指定即将发送的缓冲区内的字符数。最后,fags 可为0、MSG DONTROUTE MSG OOB。另外,flags 还可以是对这些标志进行按位“或运算的一个结果。MSG DONTROUTE 标态要求传输层不要将它发出的数据包路中出去。是否实现这请求由下尽的传输米决定(例如,若传输协议个文持该选项,这一请求就会被忽略》。MSG OOB 标志衣示数据应该进行带外发送。

在顺利的情况下 send 将返回发送的字节数:发借误,就返回 SOCKET ERROR。常见的误是 WSAECONNABORTED,这一错误一般在虚拟回路出于超时或协议有错而中断的时候发生。发生这种情况时,因为这个套接字不能再用了,所以应该将它关闭。当远程主机上的应用程序通过执行强行关闭或意外中断模作重新设置虑拟回路时,或远程主机重新启动时,发生的则是WSAECONNRESET 错误。再次需要注意的是,发生这·错误时,应该大闭这个接字。最后-个常见错误是 WSAETIMEOUT,它在连接由丁网络故障或远程连接系统并常死机而导致连接中断时发牛。
Send AP 函数的 Winsock 2 版本是 WSASend,它的定义如下:

int WSASend{
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDNORD lpNumberOfBytesSent,
DWORD dwFlags,
LPWSAOVERLAPPED lpoverlapped,
LPWSAOVERLAPPED COMPLETION RCUTINE IpCompletionRoutine
);

这个套接字是一个连接会话的有效句柄。第2个参数是指向一个或多个 WSABUF 结构的指针它既可是一个独立的结构,又可以是一个结构数组。第3个参数指明准备传递的 WSABUF 结构数量.

记住,每个 WSABUF 结构本身就代表了一个字符缓冲及其长度。为何打算同时发送多个缓冲呢? 也许人家不太明白其小的原因。这就是稍后要讲到的散播一聚集I/O 模式,但是,在·个已建立连接的套接子上利用多个缓冲来发送数据时,每个缓冲区的发送顺序都是从数组中的第一个到最后一个WSABUF 结构、IpNumbcrOBytesSent 参数是指向DWORD(是 WSASend 调用返回)的指针,其小包含已发送的字节总数。

dwFlags 参数相当于它 send 中的等效参数。最后两个参数pOverlappedlpCompletionROUTINE-用于重I/0,重叠1/0是Winsok 支持的异步/0 模式的·种。关于重1/0在第5章将进行详细讲解。

WSASend函数把IpNumberOfBytesSent 设为写入的宁节数。执行成功时,该函数就返回0,否则就返阿 SOCK ERROR,该函数的常见错误通常send 函数一样。

必须注意这最后一个发送函数WSASendDisconnect

1.6.3.2 WSASendDisconnect

这个雨数非常特殊,-般不用。其原型是:

jnt NSASendDisconnect fSOCKET S,
LPWSABUF 1pOptboundDiseonnectData

带外数据

对已建立连接的流套接宇上的应用程序来说,如果需要发送的数据比流上的普通数据重要得多,使可将这些重要数据标记成 00B(Oul-of-band,带外)数据,位于连接另一的应用程序可通过一个独立的逻辑信道(从理论上讲,该逻辑信道与数据流无关)来接收和处理 OOB 数据,在TCP中,00B 数据由一个紧急的!位标记(叫作 URG)和 TCP 分段头中的一个 16 位的指针组成。这里的标记和指针把特定的下行流宇节当作紧急数据,实现紧急数据的两种特殊方法目前只能在TCP.RFC793 中见到,该索引对 TCP 进行了描述,并引入了“紧急数据”这一概念,指明 TCP 头中的紧息指针是紧急数据字节之后那个字节的绝对偏移。在 RFC1122 中,紧急偏移被描述成指向紧急字节本身、
Winsock 规范中,独立于协议的 00B 数据和 TCP的 OOB 数(紧息数)实现均来用了OOB这一术语。要查看被搁置的数据中是否包含紧急数据,必须通过 SIOCATMARK 选项调用 joctlsocket 西教。第7章将介绍 SIOCATMARK 的用法
Winsock 提供了获得紧急数据的几种方法。可以内联紧急数据,使其出现在普通数据流中,也可以禁用内联,这样,对接收函数的不连续调用就只返回紧急数据,至于控制 OOB 数据行为的套接宇选项 SO OOBINLINE,本书也将在第 7章详细讨论
Telnet和 Rlogin 使用紧急数据是有原因的,尽管如此,除非计划编写自己的 Telnet 和 Rlogin,否刚就应该避开紧急数据,原因在于,它定义得不完善,而且它在其他平台上的实施情况可能和 Windows有所不同,如果在紧急的情况下需要一种方法来发信号通知通信方,可以为紧急数据执行独立的控制套接字,并为普通数据的传输保留主要的套接字连接
函数 WSAScndDisconnect 起初将套接字置为关闭状态,并发送断开的数。当然,它只能用于支持从容关机和断]数据的伙输协议、目前还没有传输提供科序支持断开数据。WSASendDisconnect 函数的行为和利用 SD SEND 参数调用 shutdown 两数(后面将讲到该函数)差不多,但它另外还要发送包含在 boundDisconnectData 参数中的数据,之后的发送禁在这个套接字上进行。如果调用失败,WSASendDisconnection 就会返 SOCKET ERROR。使用该函数可能会出现 send 函数中出现的某些错误。

1.6.3.3 recv和WSARecv

对于已建立连拨的套接字上接受数据的传入来说,rccv 函数是最基的方式。它的定义如下

int recv(
SOCKET e
char FAR* buf,
int len,
int flag
);

第1个参数 5,是准备用来接收数据的那个套接。第 2 个参数 buf,是于接收数据的字符缓冲区,而 len 则是准备接收的字节数或 buf 缓冲区的长度。最后的 ags 参数可以是下面的值:0MSG PEEK或MSG OOB。另外,还可对这些标点中的每一个进行按位“或”运算。当然,0表示没有特殊的行为。MSG PEEK 示要将可用的数据复制到所提供的接收端缓冲区内,但是并不从系统缓冲区中将这些数据删除。待发字节数也将被返回。

消息查看功能有一些缺点,它不仅导致性能下降(因为需要进行两次系统调用,·次是古看,另一次是无 MSG PEEK 标点的真正删除数据的调用),在某些情况下还可能不可靠。返回的数据可能没有反映出真正可用的总单。与此同时,把数据留任系统缓冲区内,可容纳传入数据的系统空间就会越来越少。结果使得系统减少各发送端的 TCP 窗容量,这样,应用程序就不能得最人的数据吐率。因此,最好是尽量把所有数都复制到自己的缓冲区中,并在那里操作数据。

在基丁消息或基于数据报的套接字(例如 UDP)使用 recv 时,儿点应该注意当挂起数据大丁所提供的缓冲区时,缓冲区会尽地计数据填满。这时,recv 调州会产生 WSAEMSGSIZE 错误。注点,消息大小的错识是在使用面向消息的协议时发生的。而流协议(如 TCP)刚把么入的数据缓下来并尽量地返国应用科序所要求的数据,即使被挂起的数据量比缓冲大。闪此,对流传输协议来说,就下金碰到 WSAEMSGSIZE 这个错误。

WSARecv 函数在 recv 的基础下增加了一些新特性。比如说叠1/0 和部分数据报通知WSARccy的定义如下:

int WSARecv (
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD IpNumberOfBytesRecvd,LPDNORD lpFlagS,
LPWSAOVERLAPPED IpOverlapped,LPWSADVERLAPPED COMPLETION ROUTINE IpCompletonRoutine
);

参数 s 是建立连接的套接。第 2 和第3 个参数是用收数据的缓冲pBuffers 参数是个由WSABUF 结构组成的数组,而dwBufferCouni 则表明这个数组中 WSABUF 结构的数量。如果接收操作立即元成,IpNumber0BytesRcccived 参数就会指向这个丽数调用所收到的字节数。lpFlags 参数可以是下面任何一个值:MSG PEEK、MSG OOB、MSG PARTIAL,者是对这些值进行按位“或”运算之后的结果。MSG PARTIAL 标志如果使和出现的地方不同,其义也不同,对支持部分消息的血向消息的协议(如 Apple Talk)水说,这个标志是在 WSARecv 调用返后设置的(如果因为缓冲区空间不够,导致整条消息未能在这次调用中返回)。这时,后的 WSARecv 调用都会设置这个标志,直到整条消息返回,才把这个MASG PARTIAL 忐清除。如果把这个标志当作·个输入参数传递则接收操作应该·–收到可用数据就结束,即使它收到的只是整条消息中的·部分,MSG PARTIAL 标志只随面向消息的协议“起使用。另外,不是所有的协议都支持部分消息。每个协议的协议条口都包含·个标志,农明是否文持这一特性,有关详情参见第2章。pOverlapped 和ipCompletionROUTINE参数用丁重叠 /O 的操作,第5 将对此进行详细讨论。此外,还必须注意另外一个特殊的接收雨数WSARecvDisconnect。

1.6.3.4 WSARecvDisconnect

这个函数与WSASendDisconnect 函数相对应,其定义如下:

int WSARecvDisconnct(
SOCKET s,
LPWSABUF lpInboundDisconnectData
);

和WSASendDisconnect 函数的参数一样,该函数的参数也是山建立连接的套接字柄,以及一个有效的 WSABUF 结构(带有接收到的数据)。接收到的数据可以只是断数。这个断开数据是另一端的 WSASendDisconnect 调用发出的,它不能用于接收普通数据。另外,一日收到数据,WSARecvDisconnect 雨数就会取消接收远程通信方的数据,其作用和用SD RECEIVE 调用 shutdown函数(稍后讲述)时相同。

1.6.4 流协议

由于大多面向连接的协议(如 TCP)同时也是流传输协议,所以,在此对流协议作一些介绍。在流协议中,发送者和接收者可以将数据分解成小块数据,或将数据合并为大块数掘。对于流套接字小收发数据所用的函数,需要了解的是:它们不能保证要求进行读取或写入的数据量。

比如说,用 send函数来发送 个有 2048 字节的字符缓冲区,采用的代码是:

char sendbuffl2048];\
int nBytes = 2048;
//用2048 字节的数填允 sendbuff
//假定 s 是一个有效的、已连接的流套接字
ret = send(s,sendbuff,nBytes,0);

对 send 函数而言,可能会返已发出的少下2048 的子节。

因为对每个收发数据的套接子来说系统都为它们分配了相当充足的缓冲区空间,所以ret 变量将被设为发送的字节数。在发送数据时内部缓冲区会将数据一直保留到可以将它发到线上为止。几种常见的情况都可导致这一情形的发生。比方说,传输大量的数据以令缓冲区快速填满。同时,对TCP/IP 来说,还一个窗口大小的问题接收端会对窗口大小进行调节,以指示它可以接收多少数据。如果有大量数据涌入接收端,接收端就会将窗口大小设为 0,为挂起的数做好准备。

对发送来说,这样会强制它在收到一个新的大下0的窗口大小之前,不得再发送数据。在使用 send 调用时,缓冲区可能只能容纳 1024 个字节,这时,使有必要重新提交剩下的 1024 个字节。要保证将所有的字节发出去,可采用下面的代码。

char sendbuff[2048];
int  nBytes = 2048, nLeft, idx;
//用 2048字节的数据填充 sendbuff
//假定 s 是一个有效的、已连接的流复接字
nLeft = nBytes;
idx = 0;
while (nLeft >0)
ret = send(s,sendbuff[idx],nLef-,0);
if (ret == SOCKET ERROR) {
//出错
}
nLeft -= ret;
idx += ret;

在流套接子上接收数据时,这一原则照样适用,但意义不大。因为流套接字是一个不间断的数据流,在读取它时,应用程序通常不会关心应该读多少数据。如果应用程序需要通过流协议获取离散消息,您需要做一些额外的T.作。如果所有消息长度都一样,则处理起来比较简单,比如说,需要读取的 512个字节的消息看起来像下面这样:

char recvduff[1024];
int  ret,nleft,idx;
nLeft = 512;
ldx = 0;
while (nLeft >0){
ret = recv(s,&recvbufflidx],nLeft,0);
if (ret == SOCKET ERROR){
//出错
}
idx += ret;
nLeft -= ret;
}

如果消息长度不同,处理起来就会麻烦-·点。因此,有必要利用自己的协议来通知接收端,让它知道即将到来的消息长度是多少。

比方说,写入接收端的前 4 个字节总是整数,大小为即将到来的消息的字节数。接收端每次开始读取时,会先食看前 4 个学节,把它们转换成一个整数,并查看构成消息的字节数是多少。

散播一聚集1/0

散播-聚集(scatter-gather) 支持是 Berkeley Socket中首次随 Recv和 Writev 这两个函数一起出现的概念,它随WSARecV、WSARecvFrom、WSASend和WSASendTo这几个 Winsock2函数一起使用对那些收发特殊格式数据的应用程序来说,这个功能是非常有用的。比方说,客户机发到服务器的消息可能一直都是这样构成的: 一个指定某种操作的固定的 32 字节的头,一个64字节的数据块和一个16字节的尾,这时,就可用一个由3个WSABUF 结构组成的数组调用 WSASend,这3个结构分别对应3种消息类型、在接收端,则用3个WSABUF 结构来调用 WSAReCV,各个结构包含的数据缓冲区分别是32字节、64字节和 16字节。
在使用基于流的套接字时,散播一聚集 I/0 模式只是把 WSABUF 结构中提供的数据缓冲区当作一个连续的缓冲区。另外,接收调用可能在所有缓冲区填满之前就返回,在基于消息的套接字上,每次对接收操作的调用都会收到一条消息,其长度由所提供的缓冲区大小决定。如果缓冲空间不够,调用就会失败,并出现 WSAEMSGSIZE 错误,为了适应可用的缓冲空间,数据就会被截断。当然,如果使用支持部分消息的协议,就可用 MSG_PARTIAL 标志来避免数据的丢失

1.6.5 中断连接

一旦完成了套接字连接,就必须将它关掉,并释放关联到那个套按字创树的所何资源。

要真正地释放与一个打开的套接宁句柄关联的资源,执行 closesocket 调用即可,但要明白这·点,closesocket可能会带来负面影响(和如何调用它有关), 即可能会导致数据的丢失。

鉴于此,应该在调用 closesocket函数之前,利用 shutdown 函数从容地终止连接。接下来讨论这两个AP[函数。

1.6.5.1 shutdown

为了保证通信方能够收到应用程序发出的所有数据,对一个好的应用程序来说,应该通知接收端“不再发送数据”。同样,通信对方也应该如此,这就是所谓的”正常关闭”方法,由 shutdown 函数来执行。shutdown 的定义如下;

int shutdown(
SOCKET s,
int how)

how参数可以是后面的任何一个值:

  • SD RECEIVE、
  • SD SEND
  • SD BOTHL:

如是SD RECEIVE,就表示不允许再调用接收函数。这对底部的协议层没有影响。另外,对 TCP 套接宁来说,不管数据在是等候接收,还是将随后抵达,都要重新设置连接。尽管如此,在 UDP 套字上仍然接受并排列传入的数据(因为对于无连接协议而言,shutdown 毫无意义)。如果选择 SE SEND,表示不允许再调用发送函数。对 TCP 套接宁来说,这样会在所有数据发出,并得到接收端确认之后生成··个FIN包。最后,如果指定 SD BOTH,则表示取消连接两端的收发操作。
应注意到,并非所有的面向连接协议都支持从容关闭,这是 shutdown API 执行的功能。对那些不支持从容关闭的协议来说(如 ATM),仪调用 closesocket 使可结束进程

1.6.5.2 closesocket

closesocket 函数用于关闭套接宁,它的定义如下:

int closesocket (SOCKET s):

对 closesocket 的调用会释放套接字描述符,然后再利用套接字执行的调用就会失败,并山现WSAENOTSOCK 错误。

如果没有对该套接字的其他引用,那么所有与套接字描述关联的资源都被释放。

其中包括丢弃所有队列中的数据,对这个进程中任何一个线程来说,它们发出的被挂起的同步调用都会在未投递任何通知消息的情况下被删除。被挂起的的重叠操作也会被删除。与该重叠换作关联的任何事件、完成例程或完成端口能被执行,但后都会失败,并出现 WSA_OPERATION_ABORTED错误。套接字I/O 模式在第5章中有详细讲解。

另外,还有一点会对 closesocket 的行为产生影响: 套接字选项 SO LINGER 是否已经设置。要了解这一点,请参考第7章中对 SO_LINGER 选项的描述。

1.7 无连接通信

和面向连接的协议比较起来,无连接协议的行为有很大不同,因此,收发数据的方法也会有所差别。和面向连接的服务器比较起来,无连接接收端改动不大,所以先讨论接收端(如果愿意,也可称之为服务器)。接下来再谈发送端。

在IP 中,无连接通信是通过 UD/P 协议完成的。UDP 不能确保可靠的数据传输,但能将数据发送到多个目标,或著接收的多个源数据。例如,如果· 个客户机向一个服务器发送数据,则数据将立刻被传输,不管服务器是否准备接收这些数据。如果服务器接收来自客户机的数据,该服务器也不会发消息确认数据已收到。数据的传输使用数据报,即离散信息包。

1.7.1 接收端

对于在一个无连接套接字上接收数据的进程来说,操作过程并不复杂先用 socket或 WSASocket创建套接字。再把这个套接字和准备接收数据的接口绑定在 -起,这是通过 bind 函数(和面向会话的示例··样)来完成的。和面向连接不同的是,不必调用 listen 和 accept。相反,只需等待接收数据。由于设有连接,始发于网络上任何一台机器的数据报都可被接收端的套接学接收。最简单的接收函数recvfrom,它的定义如下:

int recvfrom(
SOCKET 
char FAR* bufs
int len,
int flags,
struct sockaddr FAR* from,
int FAR* fromlen)

前面4个参数和 recv 的参数是一样的,其中包括标志的可能值:MSG_OOB 和MSG_PEEK。在使用无连接套接字时,和前面一样,仍然应该慎用 MSG_PEEK 标志。对监听接字的给定协议来说from参数是个SOCKADDR 结构,带有指向地址结构长度的 fromlen这个API调用的返回数据时SOCKADDR结构内便填入了发送数据的那个T作站的地址。
recvfrom 函数的Winsock 2 版本是WSARecvFrom。后者的原型是:

int WSARecvFrom(
SOCKET S,
LPWSABUF lpBuffersDWORD dwBufferCount,
LPDWORD lpNumberofBytesRecvd,
LPDWORD IpFlags,
struct sockadde FAR * IpFrOilt,
LPINT lpFromlen,
LPWSAOVERLAPPED IpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE IpCompletionRoutine
)

两者的差别在于接收数据时 WSABUF 结构的用法。可以利用 dwBufferCount 为 WSARecvFrom提供一个或多个 WSABUF 缓冲区。提供多个缓冲区后,就可以用散播一聚集 I/0了。读取的字节总数放在INumberOfBytesRecvd 中返回。

在调用WSARecvFrom 时,ipFlags 参数可以是0代表无选项).MSG 0OB、MSG PEEK或MSG PARTIAL。这些标志还可以一起按位“或”运算。如果在调用这个函数时,指定了MSG PARTIAL,提供程序就知道返回数据,即使只收到了部分消息。当调用返回之后,如果只收到部分消息,应用程序就会设置 MSG PARTIAL 标志。另外调用返回后,WSARecvFrom就会把IpFrom参数(它是-个指向 SOCKADDR结构的指针设置为发送端的地划。LpFromLn 同样指向SOCKADDR 结构的长度,不过,在这个函数中,它还是-·个指针,指向DWORD。最后两个参数lpOverlapped 和1pCompletionROUTNE,用于重叠I/O(第S章将就此展开讨论)
在无连接套接字上接收(发送)数据的另一种方法是建立连接。听起来有些奇怪吧,但事实的确如此。无连接的套接字-·旦建立,便可利用 SOCKADDR 参数(它被设为准备与之通信的远程接收端地址)调用connect 或 WSAConnect。但事实上并没有建立真正的连接。传递到连接函数的套接字地址是与接字关联在一起的,如此一来,才能够用 Rcv和WSARecv 来代替recvfrom和WSARecvFrom。

为什么呢? 其原因是数据的始发处是已知的。如果在应用程序中,一次只和一个端点进行通信,便能很容易地与数据报套接字建立连接。
下面的示例代码展示了如何创建一个简单的 UDP 接收端应用程序。本应用程序的完整版本可在补充材料中名为UDPRECEIVER的文件中找到。

#include 
void main(void){
WSADATA wsaData;
SOCKET ReceivingSocket;
SOCKADDR_IN ReceiverAddr;
int Port = 5150;
char ReceiveBuf[1024];
int BufLength = 1024;
SOCKADDR_IN SenderAddr;
int SenderAddrsize = sizeof(SenderAddr);

//初始化 Winsock 2.2 版本
WSAStartup(MAKEWORD(22)&wsaData);

//创建一个新的套接宁来接收数据报ReceivingSocket = socket(AF INET,SOCK DGRAM,IPPROTO UDP);//建立一个SOCKADDR IN结构,这个结构将告知bind 我们想要使用 5150 端口接收米自所

//有接口的数据报
ReceiverAddr.sin_famlly = AF INET;
ReceiverAddr.sin port = htons(Port};
ReceiverAddr.sin_addr.s_addr = htonl(INADDR ANY);//使用 bind 将这个地址信息和套接字关联起来bind(ReceivingSocket,(SOCKADDR *)5SenderAddr,sizeof(SenderAddr));
//此时可以在绑定套接字上接收数据报了
recvfrom(ReceivingSocket,ReceiveBuf,BufLength,0,(SOCKADDR *)sSenderAddr,&SenderAddrsize);
//应用程序结束接收数据报后,关闭套接字
closesocket(ReceivingSocket)
//应用程序结束后,调用 WSACLeanup
WSACleanup();

在了解到如何创建一个能够接收数据报的接收端之后,下面叙述怎样创建发送端。

1.7.2 发送端

要在一个无连接的套接字上发送数据,有两种选择。最简单的一种,使是建立一个套接字,然后调用 sendto或WSASendTo。先讲解 sendto 函数,它的定义是这样的:

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

除了 buf是发送数据的缓冲区,len 指明发送多少字节外,其余参数和 recvfrom 的参数-·样。另外,0参数是一个指向SOCKADDR 结构的指针,该结构带有接收数据的那个丁作站的目标址址。另外,也可以用Winsock 2 函数WSASendTo。

它的定义如下:

int WSASendTo(
	SOCKET S,
	LPWSABUF lpBuffers
	DWORD dwBufferCount,
	LPDWORD lpNumberofBytesSent,
	DWORD dwFlags,
	const struct sockaddr FAR * IpTo,
	int iToLen,
	LPWSAOVERLAPPED IpOverlapped,
	LPWSAOVERLAPPED_COMPLETION_ROUIINE IpCompletionRoutine
);

同样,WSASendTo和前版本中的 sendto 函数类似。它把指针当作 ipBuffers 参数,该指针指向带有发给接收端数据的结构,而dwBufferCount 参数则指明当前有多少个结构。发送多个WSABUF结构来启用散播-聚集/0。在函数返回之前,WSASendTo 把第4个参数pNumber0BytesSent设为真正发送到接收端的早节数。IpTo 参数是针对具体协议的个 SOCKADDR 结构,并带有接收端的地址。iToLen 参数是SOCKADDR 结构的长度。最后两个参数,IpOverlapped 和ipCompletionROUTINE用于重叠 I/0(将在第5中讨论)。

通过接收数据的方式,就可以把一个无连接的套接子连接到一个端点地址,并以用 send 和WSASend发送数据。这种关联旦建立,就不能再用带有地址的 sendto 和 WSASendTo,除非这个地址足传递到其中 个连接函数的地址,否则调用就会失败,出现 WSAEISCONN 错误。要取消套接宁柄与目标地址的关联,惟一的办法是在这个套接字句柄上以 INADDR ANY 为目标地址调用connect。

下面的示例代码展示了如何创建一个简单的 UDP 发送端应用程序。本应用程序的完整版本可在配套光盘中名为UDPSENDER 的文件中找到。

// Module Name: udpsender.cpp
//
// Description:
//
//    This sample illustrates how to develop a simple UDP sender application
//    that can send a simple "hello" message to a UDP receiver awaiting datagrams
//    on port 5150. This sample is implemented as a console-style application and 
//    simply prints status messages as data is sent to the server.
//
// Compile:
//
//    cl -o udpsender udpsender.cpp ws2_32.lib
//
// Command Line Options:
//
//    udpsender.exe  
//


#include 
#include 

void main(int argc, char **argv)
{
   WSADATA              wsaData;
   SOCKET               SendingSocket;
   SOCKADDR_IN          ReceiverAddr;
   int                  Port = 5150;
   int                  Ret;

   if (argc <= 1)
   {
      printf("USAGE: udpsender .\n");
      return;
   }

   // Initialize Winsock version 2.2

   if ((Ret = WSAStartup(MAKEWORD(2,2), &wsaData)) != 0)
   {
      // NOTE: Since Winsock failed to load we cannot use WSAGetLastError 
      // to determine the error code as is normally done when a Winsock 
      // API fails. We have to report the return status of the function.

      printf("ERROR: WSAStartup failed with error %d\n", Ret);
      return;
   }
   
   // Create a new socket to receive datagrams on.
 
   if ((SendingSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP))
       == INVALID_SOCKET)
   {
      printf("ERROR: socket failed with error %d\n", WSAGetLastError());
      WSACleanup();
      return;
   }

   // Setup a SOCKADDR_IN structure that will identify who we
   // will send datagrams to. For demonstration purposes, let's
   // assume our receiver's IP address is 136.149.3.29 and waits
   // for datagrams on port 5150. Obviously you will want to prompt 
   // the user for an IP address and port number and fill these 
   // fields in with the data from the user.

   ReceiverAddr.sin_family = AF_INET;
   ReceiverAddr.sin_port = htons(Port);    
   ReceiverAddr.sin_addr.s_addr = inet_addr(argv[1]);

   // Send a datagram to the receiver.

   if ((Ret = sendto(SendingSocket, "Hello", 5, 0, 
       (SOCKADDR *)&ReceiverAddr, sizeof(ReceiverAddr))) == SOCKET_ERROR)
   {
      printf("ERROR: sendto failed with error %d\n", WSAGetLastError());
      closesocket(SendingSocket);
      WSACleanup();
      return;
   }

   // When your application is finished sending datagrams close
   // the socket.

   printf("We successfully sent %d byte(s) to %s:%d.\n", Ret,
          inet_ntoa(ReceiverAddr.sin_addr), htons(ReceiverAddr.sin_port));

   closesocket(SendingSocket);

   // When your application is finished call WSACleanup.

   WSACleanup();
}


1.7.3 基于消息的协议

正如面向连接的通信同时也是流协议,无连接通信几乎都是基于消息的。因此,在收发数据时,需要考虑这几点。首先,由于面向消息的协议保留了数据边界,所以提交给发送函数的数据在被发送完之前会形成阻塞。对非阻塞 I/0 模式而言,如果数据未能完全发送,发送函数就会返四WSAEWOULDBLOCK 错误。这意味着下层的系统不能对不完整的数据进行处理,应该稍后再次调用发送函数。第5 章将对此进行详述。需要记化的是,对丁基于消息的协议而言,写入操作只能作为一种自动行为发生。

在连接的另一端,对接收雨数的调用必须提供一个足够大的缓冲空间。如果提供的缓冲区不够大,接收调用就会失败,将出现借误 WSAEMSGSIZE。发生这种情况时,缓冲区会填满,但未接收完的数据会被丢弃。被截断的数据也无法恢复。唯 的例外是支持部分消息的协议,比方说 AppleTalkPAP协议。当接收调用仅接收到部分消息时,WSARecv、WSARecEx 或WSARecvFrom函数会在返网之前,将出入 flag 参数设为 MSG PARTIAL。

对以支持部分消息的协议为基础的数据报来说,可考虑使用某个 WSARecv 函数因为在调用 recy或recvfrom 时,不会有“读取的数据只是消息的-·部分”这样一个通知。至丁接收端怎样判断是否已卖取完整条消息,具体方法则由程序员决定。

随后的recy/recvrom 调用将返回这个数据报的其余部分由十有这个限制,利用 WSAReCvEx 函数就显得非常方便,因为它允许设置和读取 MSG PARTIAL标志,而 MSG PARTIAL 标志指明整条消息是否已读取完毕。

Winsock 2 函数 WSARecy 和WSARecvFrom 也支持这一标志。关于这个标志的更多内容,请参见对 WSARecv、WSARecvEx和WSARecvFrom 这3个函数的描述最后要讲的使是在有多个网络接口的机器发送 UDP/IP 消息。这方面的问题颇多,先来看个最常见的问题:在·个 UDP 套接字被显式地绑定到-·个地 P 接口,并发送数据报时,会发生什么情况?UDP 套接字并不会真正和网络接口绑定在“起,而是建立一种关联,即被绑定的[P 接口成为发出去的 UDP数据报的源]P 地址。真止决定数据报在哪个物接口上发送出去的,是路由表。如柴不调用 bind,而是先调用 sendto/WSASendTo 或执行连接,网络堆栈就会根据路由衣,自动选出最件本地 IP 地址。这意味着,如果先执行显式绑定,源IP 地址就可能误。也就是说,源 可能不是具正存它上面发送数据报的那个接口的 IP 地址。

1.7.4 释放套接字资源

因为无连接协议没有连接,所以也不会有对连接的正式关闭和从容关闭。在接收端或发送端完成收发数据时,它只需要在套孩了句柄上调用 closesockel 函数,使可释放为套接字分配的所有相关资源。

1.8 其他API函数

本小节介绍其他几个 Winsock API 函数,它们在网络应用程序中非常有用

1.8.1 getpeername

该函数用于获得通信方的套接字地址信息,该信息是关于已建立连接的那个套接字的。它的定义如下:

int getpeername (
SOCKET S,
struct sockaddr FAR* name,
int FAR* namelen)

第1个参数是准备连接的套接字,后两个参数则是指向下层协议类型的SOCKADDR 结构及其长度的指针。对数据报套接字来说,这个函数返回的是传递到连接调用的那个地址:但不会返回传递到sendto或WSASendTo 调用的那个地址。

1.8.2 getsockname

该函数是对应 getpeername 的雨数。它返回的是给定接字的本地接 的地址信息。

它的定义如下

int getsockname(
SOCKET S,
struct sockaddr FAR*name,
int FAR* namelen,
)

除了套接字 s 返回的地址信息是本地地址信息外,其参数和 getpeername 的参数都是一样的。在TCP 协议中,这个地址与监听特定端口及 IP 接口的那个服务器套接字是一样的。

1.8.3 WSADuplicateSocket

WSADuplicateSocket 函数用来建立WSAPROTOCOLINFO 结构,该结构可传递到另一个进程这样就可用另个进程打开指向同一个下层套接字的句柄,如此一来,这个进程也能对该资源进行探作。

注意,这,点只在进程之间的操作时才必要,同一个进程中的线程可自由传递套接字描述符该函数的定义如下:

int WSADuplicatesocket(
SOCKET S,
DWORD dwProcessId,
LPWSAPROTOCOL_INFO IpProtocolInfo
)

第1个参数是准备复制的套字第 2个参数 dwPrcessld,是打算使所复制的会接字的进程的ID。第3个参数lpProtocolInfo,是–个指向WSAPROTOCOL INFO结构的指针,该结构包含目标进程打开复制句柄时所需的信息。为了使当前的进程能够把 WSAPROTOCOL INFO 结构传递给目标进程,然后月标进程再利用该结构建立·个指向指定套接字的句柄(利用 WSASocket 函数),必须考虑进程间通信。

两个套接字的描述符都可独立使用 I/O;但 Winsock 没有提供访问控制,因此这要由程序员决定是否执行某种同步。因为是复制的套接字描述符,而不是实际的套接字,所以所有描述符中都可见到关联到某个套接字的所有状态信息。比方说,对于描述符上由 setsocketopt 函数设置的任何一个套按字选项,都可通过任何一个或所有描述符利用 getsockopt 函数来查看它们。如果一个进程在一个复制的套接字上调用 closesocket,就会导致该进程中的描述符被释放:但在最后留下的那个描述符上调用closesocket 之前,下层套接宇会保持打开状态。

1.9 Windows CE

前面几个小节中的所有信息中也都可以应用于 Windows CE。惟一例外的是由于 Windows CE是建立在 Winsock 1.1 规范基上的,因此不能用 Winsock 2中特有的函数,比如用于收发数据、建立连接和接受连接的函数的 WSA 变体。Windows CE 平台上可用的 WSA 函数只有 WSAStartupWSACleanup、WSAGetLastError 和 WSAloctl,本章经对前 3 个函数进行了讨论 最后一个留到第7章作全面讲解。

Windows CE 支持 TCP/IP 协议,这意味着可以同时访 TCP 和UDP除了 TCP/IP 之外,它还支持红外线套接字。IrDA 协议只支持面向流的通信。

对这两个协议来说,所有的常用 Winsock 1.1 API函数调用都可用于创建和传输数据。惟一例外的操作是必须解决 Windows CE 2.0 内 UDP 数套字中的销误:即每次调用send sendto都会导致核心内存泄漏。这个错误在 Windows CE 2.1中已得到修复,但出上内核分散在 ROM 中,因此,目前还没有更新的软件可以像复 Widows CE 2.0 中的这一错。惟一的解决办法是不要在 Windows CE 2.0中使用数据报。

由于Windows CE 个支持控制台应用程序,而且只采用 UNICODE,所以本章介绍的示例都以Windows 95、Windows 98、Windows NT 平台为目标平台,举出这些例子的目的是让人家直接了解Winsock 的核心概念,用不着理会那些与 Winsock 无关的代码。除非此时正在编写 Windows CE 服务程序,否则几乎随时都需要一个用户界面,这样,使需要为窗口处理程序和其他用户界面元素编写若F新函数,而这样就违背了本书的初衷。除此以外还必须了解 UNICODE 和UNICODE 的 Winsock函数。至于传递给收发 Winsock 函数的指定字符串尼UNICODE 字符串还是ANSI字符串,则山程序员决定。只要传递的是一个效的缓冲,Winsock 就不会注意其内容(当然,可能需要对缓冲类型进行转换以抑制编详警告)。需要记住,如果把一个 UNICODE 字符串转换成 char*,那么准备发送多少字节的长度就应该对参数做出相应的调整。因为其他的 Widows 系统函数都要求 UNICODE 字符中所以在 Windows CE 中,如果打算把收到或发出的数据显示出来,必须考虑到它是不是 UNICODE,这样才能将它显示出米。总而言之,对于创建一个简单的 Winsock 应用程序而言,Windows CE 要复杂得多。

如果想在 Windows CE 上运行这些示例,只需要稍微修改 Winsok 代码即可,首先,头文件必须是WINSOCKH,而不是WINSOCK2.H为 Winsock 1.1 是适用于 Windows CE 的最新 Winsock 版本,所以WSAStartup 应该加它。

另外,Windows CE 不支持控制台应用程序,因而必须用WinMain来代 main。注意,这并不意味着需要把一个窗口合开到应用程序中,只意味着不能用 printf 这一类的控制台文本 I/O函数。

总结

本介绍了如何具体使用 TCP 和 UDP 协议以建立面向连接通信和无连接通信时所需的核心Winsock 函数。

针对面向连接通信,本演示了如何接受个客户机连接,以及怎样建立一个到服务器的客户机连接。

另外还介绍了面向会话的数据收发操作。针对无连接通信,本章也讲到了如何收发数据,由于本章的目的是介绍核心的 Wimsock API,因此并未考虑涉及网络编程性能方面的问题。

第6章将涉及性能方面的问题,并介绍微软 Winsock 的扩展,包括 TransmitFile、TransmitPackets、AcceptEx、GetAcceptExSockaddrs、ConnectEx、DisconnectE 以及 WSARecvMsg 等。

这些函数将有助于编写高性能、可升级的 Winsock 应用程序前面的讨论已经说明了如何用 Ipv4 协议来使用 WinSock,随后3 个章节将讲述 Winsock 结构的设计,并介绍其他可用协议的使用方法。

参考:
Microsoft Windows网络编程 第一章Winsock简介


私信回复:
windows网络编程pdf
可获得【Microsoft Windows网络编程】pdf版


关于博主

wx/qq:binary-monster/1113673178
wxgzh: 二进制怪兽
CSDN:https://blog.csdn.net/qq1113673178
码云:https://gitee.com/shiver
Github: https://github.com/ShiverZm
个人博客:www.shiver.fun

你可能感兴趣的:(windows,读书笔记,笔记,microsoft,windows)