1 Winsock简介
1.1 Winsock头文件及库文件
-
Winsock有两个版本:Winsock1和Winsock2,都能在WinCE之外的Windows系统运行。
WinCE只支持Winsock1。
1.2 Winsock初始化
int WSAStartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
);
- 函数解释
- 参数
- wVersionRequested:指定准备加载的Winsock库的版本,其中高位字节表示次版本,低位字节表示主版本。可以使用MAKEWORD(x,y)获得,x:高,y:低。
- lpWSAData:一般以需要设置前面两个成员即可。
- 参数
1.3 错误检查和处理
int WSAGetLastError(void);
int WSASetLastError(void); // ?
1.3 协议寻址
- 协议
- IP:是一种无连接协议,不能确保数据传输的成功;
- TCP和UDP使用IP进行面向连接和无连接的数据通信。所以称作TCP/IP和UDP/TP。
- IPv4寻址
- 通过
SOCKADDR_IN
结构来指定IP地址和服务端口信息。- sin_family字段必须设置为AF_INET,已告知Winsock此时正在使用IP地址族。
- sin_port定义端口。
- sin_addr保存IP地址。
-
inet_addr
函数:把点分IP地址转换成32位无符号长整数。 - 字节排序
-
big-endian
:字节的排序从最无意义的字节到最有意义的字节; -
little-endian
:字节的排序从最有意义的字节到最无意义的字节; - 主机字节(host-byte):主机实际字节顺序。
- 网络字节顺序:即big_endian。
- 常用函数
- 主机字节序转网络字节序
u_long htonl(u_long hostlong);
int WSAHtonl(SOCKET s, u_long hostlong, u_long FAR* lpnetlong);
u_short htons(u_short hostshort);
WSAHtons(SOCKET s, u_short hostshort, u_short FAR* lpnetshort);
- 网络字节序转主机字节序
u_long ntonl(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, ushort FAR* lphostshort);
- 主机字节序转网络字节序
-
- 通过
1.5 创建套接字
-
SOCKET
SOCKET socket{ int af;// 协议地址族,IP为AF_INET int type;// 协议的套接字类型,TCP:SOCK_STREAM,UDP:SOCK_DGRAM int protocol;// TCP:IPPROTO_TCP,UDP:IPROTO_UDP };
Winsock两种基本通信技术:面向连接的通信和无连接的通信。
1.6 面向连接的通信
1.6.1 服务器API函数
1.6.1.1 绑定
一旦为某种协议创建了套接字,就必须将套接字绑定到一个已知的地址上。
-
bind函数
int bind( SOCKET s, // 等待客户连接的服务器套接字 const struct sockaddr FAR* name, // 地址缓冲区 int namelen );
-
bind常见错误
- WSAEADDRINUSE:如果是TCP/IP,那么说明另一个进程已经同本地IP接口及端口号绑定了,或者那个IP接口和端口号处于TIME_OUT状态。
- WSAEFAULT:说明该套接字已被绑定。
1.6.1.2 监听
-
listen函数
int listen( SOCKET s,// 一个已经被绑定的套接字 int backlog // 指定了被搁置的连接的对大队列长度 );
backlog:
- 假定该参数为2,那么当有3个客户机同时发出请求,那么前面两个会被放在一个“挂起”队列中,而第三个连接请求会失败。并返回WSAECONNREFUSED错误码。
- 当服务器接受了一个连接,那么该连接就会从队列中删除,以便别人可以继续发出请求。
- 其本身也有限制,该限制是由下层的协议提供程序决定的。如果这个参数出现非法值,那么系统会用与之最接近的一个合法值来取代。但是如何找出实际的backlog值,还不存在一种标准的方案。
1.6.1.3 接受连接
-
accept函数
SOCKET accept( SOCKET s, // 一个已经被绑定的套接字 struct socketaddr FAR* addr, int FAR* addrlen );
-
函数说明
- accept函数返回后,addr结构中会包含发出连接请求的那个客户机的IPv4地址信息,而addrlen参数则指出addr结构的长度。
- accept函数会返回一个新的套接字描述符,对应已经被接受的那个客户机连接。
1.6.2 客户端API函数
客户端创建步骤:
- 创建套接字;
- 建立一个SOCKADDR地址结构,结构名称为准备连接的服务器名。
- 用connect或WSAConnect初始化客户机与服务器的连接。
-
TCP状态
- 套接字状态:CLSOED、ESTABLISHED、SYN_SENT、SYN_RCVD等。
- 套接字主动关闭以及被动关闭。
-
connect函数
int connect( SOCKET s, const struct socketaddr FAR* name, int namelen );
常见错误码:
- WSAECONNREFUSED:想要连接的计算机没有用于监听的进程;
- WSAETIMEOUT:试图连接的计算机不可用(也有可能是硬件故障,或主机没有联网等);
1.6.3 数据传输
常见错误:
- WSAECONNRESET:连接正在被关闭,可能是超时、通信方关闭连接。
- WSAEWOULDBLOCK:一般套接字处于非阻塞模式或异步状态时会返回。
1.6.3.1 send
和WSASend
-
send函数
int send( SOCKET s, const char FAR* buf, int len, int flags );
常见错误
- WSAECONNABORTED:一般在虚拟回路由于超时或协议有错而中断是发生。此时表明该套接字不能在使用,应将其关闭。
- WSAECONNRESET:远程主机上的应用程序通过执行强行关闭或意外中断操作重新设置虚拟回路,或远程主机重新启动。也需要关闭该套接字。
- WSAETIMEOUT:网路故障或远程连接系统异常死机而导致连接中断。
1.6.3.2recv
和WSARecv
-
send函数
int recv( SOCKET s, char FAR* buf, int len, int flags );
常见错误
- WSAEMSGSIZE:当挂起数据大于所提供的缓冲区,缓冲区会尽量让数据填满。此时recv调用会产生该错误,但该错误只会在UDP协议出现,TCP不会。
注意:慎用MSG_PEEK标记。
1.6.4 流协议
1.6.5 中断连接
从容关闭连接的套路是先调用shutdown
函数后再调用closesocket
函数。
1.6.5.1 shutdown
函数
-
shutdown函数
int shutdown( SOCKET s, int how );
该函数对UDP而言毫无意义,因为它是无连接的。
1.6.5.2 closesocket
函数
-
closesocket函数
int closesocket(SOCKET s);
1.7 无连接通信(UDP/IP)
UDP不能确保可靠的数据传输,但能将数据发送到多个目标,或者接受的多个源数据。
数据的传输使用数据报,即离散信息包。
1.7.1 接收端
-
步骤
- 用socket或WSASocket创建套接字;
- 把套接字和准备接收的数据绑定在一起(bind函数);
- 调用接收函数。
-
接收函数
-
rcvfrom
函数int recvform( SOCKET s, char FAR* buf, int len, int flags, struct sockaddr FAR* from, int FAR* fromlen );
注意:慎用MSG_PEEK标记。
-
WSARecvFrom
函数略
-
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 );
-
WSAendTo
函数略
-
1.7.3 基于消息的协议
面向连接的通信同时也是流协议,无连接通信几乎都是基于消息的。
-
收发数据考虑点
-
面向消息的协议保留了数据边界,所以提交给发送函数的数据在发送完之前会形成阻塞。对非阻塞I/O模式而言,如果数据未能完全发送,发送函数就会返回WSAEWOULDBLOCK错误。需要再次调用发送函数。
记住:对于基于消息的协议而言,写入操作只能作为一种自动行为发生。
连接的另一端,对接收函数的调用必须提供一个足够大的缓冲空间。否则会出现WSAEMSGSIZE错误。此时缓冲区会被填满,且未接收的数据会被丢弃。且无法恢复。(唯一特例是支持部分消息的协议)
-
多个网络接口机器上发送UDP/IP消息问题。
UDP套接字不会真正和网络接口绑定在一起,而是建立一种关联,即使用
bind
显示绑定IP接口称为发出去的UDP数据报的源IP地址。但是真正决定数据报在哪个物理接口上发送出去的,是路由表。意味着,如果先执行显式绑定,源IP地址就可能有误,即源IP地址可能不是真正在它上面发送数据报的那个接口的IP地址。
书上并没有把问题讲清楚,需后续在查找资料。
-
1.7.4 释放资源
本来就是无连接,对它不用太客气,直接调用closesocket
函数即可。
1.8 其他API函数
1.8.1 getpeername
函数
用于获取通信方的套接字地址信息。
1.8.2 getsockname
函数
该函数对应getpeername
函数。返回给定套接字的本地接口地址信息。
1.8.3 WSADuplicateSocket
函数(套接字复制函数)
用于建立WSAPROTOCOL_INFO结构,该结构可以传递至另一个进程。这样另一个进程打开指向同一个下层套接字的句柄,如此一来,这个进程也能对该资源进行操作。
该函数生成的套接字描述符被称为共享套接字描述符。
注意点:
- 需考虑进程间通信问题;
- 需考虑I/O同步问题;
- 使用该函数复制出的描述符的进程关闭该套接字,不会影响其他进程。直到最后一个描述符调用关闭套接字之前,下层套接字依然保持打开状态。
- 利用任何一个共享描述符执行
WSAAsyncSelect
函数或WSAEventSelect
函数时,都会删除以前所有的套接字事件注册。
1.9 Windows CE
2 设计Winsock
2.1 系统体系结构
提供程序:具体执行应用程序的Winsock API调用的程序。具体调用哪个提供程序,由WS2_32.dll确定。
-
提供程序分类
基础提供程序:位于传输层顶端
-
分层提供程序:
位于WS2_32.dll之下,基础提供程序之上;
-
能截获并操纵Winsock调用;
如果应用程序利用分层提供程序创建了一个套接字,分层提供程序将使用该套接字截获所有的Winsock调用。分层提供程序可能会阻塞、修改调用,也可能将未修改的调用传递给底层的提供程序。
2.2 协议的特征
2.2.1 面向消息
如果某种协议将字节当做一条独立的消息在网上传输,则称该协议是面向消息的。如UDP。
保留消息边界:发送端向接收端发送三个数据报,接收端在第一次接收时,并不会把所有的三个数据报返回,即使它已经全部接收。
2.2.1 面向流
不保留消息边界的协议通常被称作“基于流的协议”。
流服务的定义是连续的数据传输;
- 对于接收端:不管消息边界是否存在,接收端都会尽量的读取有效数据;只要数据一到达网络堆栈,网络堆栈就开始读取它,并将它放在缓冲区中等待进程处理。
- 对于发送端:系统会将原始消息分解成小消息或者把几条消息积累在一起,并形成一个较大的数据包。
2.2.3 伪流
对于发送端:依然是发送离散消息;跟面向消息一样。
对于接收端:将接收到的数据放在缓冲池中,接收应用程序可随意读取;类似面向流。
2.2.4 面向连接和无连接
通常,一个协议要么提供面向连接的服务,要么提供无连接的服务。
- 面向连接:进行数据交换之前必须建立一条路径。
- 无连接:不在乎接收端是否在侦听,直接发。
2.2.5 可靠性和有序性
面向连接可靠性高,也可保证有序性。但是二者不可兼得。
2.2.6 正常关闭
只出现在面向连接的协议中。
2.2.7 广播数据
即从一个工作站发出的数据,局域网内的所有工作站都能收到。
2.2.8 多播数据
指一个进程发送数据而一个或多个接收端可以接收到数据的能力。
2.2.9 服务质量
体现了应用程序请求独占网络带宽的能力。
2.2.10 部分消息
只用于面向消息的协议。
2.2.11 路由选择的考虑
路由器会直接丢弃非路由协议数据包,即使两台机器连接在同一路由器。
2.2.12 其他特征
2.3 Winsock编录(Winsock目录)
Winsock编录是一个数据库,它包含系统中可用的各种不同的协议。
-
WSAEnumProtocols
函数:获取系统中安装的网络协议的相关信息。
2.3.1 Winsock编录和Win64
64位应用程序使用64位Winsock编录;
32位应用程序使用32位Winsock编录。
2.3.2 创建套接字
- 多个提供程序可以共享一个地址族、套接字类型及协议。需使用
WSASocket
函数代替socket
函数。
3 网际协议
3.1 IPv4
3.1.1 寻址
-
IPv4地址分类
种类 网络部分 第1个数字 端点数字 A 8位 0-127 16 777 216 B 16位 128-191 65 536 C 24位 193-223 256 D N/A 224-239 N/A E N/A 240-255 N/A 例:172.31.28.120/16
解析:地址的前16位表示地址的网络部分,相当于子网掩码为255.255.0.0。
127.0.0.1:环回地址,指向本地计算机。
3.1.1.1 单播
分配到单个计算机接口上的地址称为单播地址,该地址仅可以分配到一个接口上。A/B/C3类地址组成IPv4的单播地址空间。
3.1.1.2 多播
多播地址是D类地址。
3.1.1.3 广播
受限广播地址:255.255.255.255。
3.1.2 IPv4管理协议
IPv4需要依赖其他协议才能实现其功能。
-
ARP(Address Resolution Protocol):地址解析协议
将一个32位IPv4地址解析为一个物理地址或硬件地址,使用IPv4数据包能够被包装在适当的媒体帧中。
- 如果目标地址位于局域网内,则ARP请求将针对目的地的物理地址。
- 如果一个或者多个路由器将源和目的地分离开来,则ARP请求将针对缺省网关,数据包也将发往该网关。
-
ICMP(Internet Control Message Protocol):Internet控制信息协议
目的是为了在IPv4主机之间发送状态信息和错误信息。也被用来搜寻邻近路由器。
-
IGMP(Internet, Management Protocol):Internet组管理协议
用来管理多播组的成员。
3.1.3 Winsock中的IPv4寻址
- 端口号分类
- 0~1023:由IANA控制,为已知服务所保留;
- 1024~49151:已注册端口号,由普通用户执行的普通用户进程或程序可以使用这些端口;
- 49152~65535:动态和(或)专用端口。
- 如果没有显示绑定端口号,系统会把套接字隐式地绑定到1024~5000范围内的一个本地端口上。
3.2 IPv6
-
NAT(Network Address Translators):网络地址转换器。
将多个专用地址映射到单个的公共IP地址上。
该节略。
3.3 地址及名称解析
3.3.1 名称解析例程
-
名称解析函数
getnameinfo
函数和getaddrinfo
函数:可以兼容IPv4和IPv6,用以代替gethostbyname
函数和inet_addr
函数。 使用:包含
WS2TCPIP.H
头文件,并在其之前包含WSPIAPI.H
头文件。
该章略过,后面主要讲述一些使用新函数的示例代码。
4 Winsock支持的其他协议(略过)
5 WinsockI/O方法
5.1 套接字模式
5.1.1 阻塞模式
I/O操作完成前,执行操作的Winsock调用会一直等待下去。
5.1.2 非阻塞模式
Winsock函数无论如何都会立即返回。
5.2 套接字I/O模型
5.2.1 阻塞模型
优点:简洁;
缺点:浪费资源,难以扩展到很多连接的情况。
5.2.2 select模型
工作原理是利用select
函数实现对I/O的管理。
优点:从单个线程的多套接字上进行多重连接及I/O操作。避免阻塞模式的线程剧增。
缺点:也是单线程的缺点一般最大不超过1024的套接字。
5.2.3 WSAAsyncSelect模型
消息通知:应用程序可在一个套接字上接收以Windows消息为基础的网络事件。
-
WSAAsyncSelect
函数int WSAAsyncSelect( SOCKET s, // 发出网络事件的套接字 HWND hWnd, // 接收网络事件的窗口 unsigned int wMsg, // 发生网络事件时接收的消息 long lEvent // 代表一个掩码,指定一系列网络事件的组合 );
注意:
- 多个事件必须在套接字上一次完成注册;
- 除非明确调用
closesocket
函数,或者调用WSAAsyncSelect
函数更改注册的网络事件,否则,时间通知总是有效; - 将
lEvent
参数置为0,则效果相当于停止在套接字上进行的所有网络事件。
调用
WSAAsyncSelect
函数后,套接字的模式会从阻塞模式自动变成非阻塞模式。-
FD_WRITE发出条件:
- 使用```connect``或者WSAConnect,一个套接字首次建立连接;
- 使用accept或者WSAAccept,套接字被接受以后;
- 若send、WSASend、sendto或WSASendTo操作失败,返回了WSAEWOULDBLOCK错误,而且缓冲区的空间变得可用。
优点:在系统开销不大的情况下同时处理许多连接。
缺点:即使应用程序不需要窗口,也必须创建一个窗口;且一个窗口处理成千上万的套接字中的所有事件,性能成为瓶颈。
5.2.4 WSAEventSelect模型
事件通知:通过事件对象句柄完成网络事件处理。
-
步骤
// 创建事件对象(人工重设事件对象) // 两种工作状态:signaled/non-signaled // 两种工作模式:manualreset/autoreset WSAEVENT WSACreEvent(void); // 注册事件消息通知 int WSAEventSelect( SOCKET s, WSAEVENT hEventObject, long lNetWorkEvents ); // 修改事件工作状态 BOOL WSAResetEvent(void); // 关闭事件 BOOL WSACloseEvent(WSAEVENT hEvent); // 等待一个或多个网络事件对象句柄 DWORD WSAWaitForMultiEvent( DWORD cEvents, const WSAEVENT FAR* lphEvent, BOOL fWaitAll, DWORD dwTimeout, BOOL fAlertable );
优点:概念简单;不需要窗口环境。
缺点:最多只支持64个事件。扩充只能通过住址线程池解决。
5.2.5 重叠模型(该节略过)
可通过ReadFile和WriteFile两个函数实现在设备上执行I/O操作。但是考虑到跨平台性,建议使用WSARecv和WSASend函数。
- 步骤
- 创建套接字;
- 将套接字与本地接口绑定;
- 可通过事件对象通知和完成例程来管理重叠I/O请求的完成情况。
5.2.5.1 事件通知
要求将Windows事件对象与WSAOVERLAPPED结构关联。
5.2.6 完成端口模型(该节略过)
6 可伸缩的Winsock应用程序(略过)
6.1 API及可伸缩性
6.1.1 AcceptEx
6.1.2 GetAcceptExSockaddrs
6.1.3 TransitFile
6.1.4 TransmitPackets
6.1.5 ConnectEx
6.1.6 DisconnectEx
6.1.7 WSARecvMsg
6.2 可伸缩的服务器体系结构
6.2.1 接受连接
6.2.2 数据传输
6.3 资源管理
6.4 服务器策略
6.4.1 高吞吐率
6.4.2 最大化连接数
6.4.3 性能指标
6.5 Winsock直连及套接字直连协议
7 套接字选项和I/O控制指令
7.1 套接字选项
7.1.1 SOL_SOCKET选项级别
7.1.2 SOL_APPLETALK选项级别
7.1.3 SOL_IRLMP选项级别
7.1.4 IPPROTO_IP选项级别
7.1.5 IPPROTO_IPV6选项级别
7.1.6 IPPROTO_RM选项级别
7.1.7 IPPROTO_TCP选项级别
7.1.8 NSPROTO_IPX选项级别
7.2 IOCTLSOCKET、WSAIOCTL和WSANSPloctl
8 名称注册和解析
8.1 背景知识
名称注册是一个过程,它把一个用户友好的名称和具体协议地址关联在一起。
8.2 命名空间模型
- 命名空间提供用一个友好名把具体的协议及其寻址属性关联在一起的功能。如针对IP的DNS。
- 分类
- 动态命名空间:允许实时注册服务;
- 静态命名空间:须手动注册,无法使用Winsock用它来注册服务名;
- 固定命名空间:允许实时注册服务;和动态命名空间不同的是它把注册信息保留在固定地方。
- WSAEnumNameSpaceProvider:获取系统上所有的命名空间。
8.3 服务的注册
需要利用命名空间提供程序注册服务。
服务类**:描述哪些命名空间可用来注册服务;
服务:服务是啥???TODO
8.3.1 安装服务类
函数:WSAInstallServiceClass
8.3.2 服务的注册
函数:WSASetService
8.3.3 服务注册示例
8.4 服务的查询
WSALookupServiceBegin
WSALookupServiceNext
WSALookupServiceEnd
8.4.1 怎样查询服务
8.4.2 查询DNS
8.4.3 查询NLA
9 多播
9.1 多播的含义
找到多播属性
9.2 IP多播
9.3 可靠多播
9.4 ATM多播
10 常规服务质量
10.1 背景知识
10.1.1 RSVP
10.1.2 网络组件
10.1.3 应用组件
10.1.4 策略组件
10.2 QOS和Winsock
10.3 终止QOS
10.4 QOS编程
10.5 示例
10.6 ATM和QOS
11 原始套接字
利用原始套接字可以访问位于基层的传输层协议。利用原始套接字来模拟IP的一些实用程序,如Traceroute和Ping等。
11.1 创建原始套接字
11.2 ICMP
便于不同主机之间传递消息的一种机制。
11.2.1 Ping示例
11.2.2 Traceroute示例
11.3 使用IP头包含选项
12 Winsock服务提供程序接口(SPI)
- 分类
- 传输服务提供程序
- 分层服务提供程序:
- 会将自己安装在Winsock编目里,并位于基础服务提供程序之上,可能位于其他分层提供程序之间。
- 截获应用程序对Winsock API调用;
- 如果应用程序创建的套接字和某个分层提供程序的特征匹配,则该分层服务提供程序就会被调用。
- 基础服务提供程序:公开一个Winsock接口,直接执行一种协议。
- 分层服务提供程序:
- 命名空间提供程序
- 类似传输服务提供程序,只是它截获的是名称解析的Winsock API调用,如gethostbyname或WSALookupServiceBegin;
- 将自己安装在命名空间编录中,当应用程序执行的名称解析搜索和它匹配时,命名空间提供程序就会被调用。
- SPI函数前缀
- WSC:安装、删除或修改分层提供程序及命名空间提供程序;
- WSP:分层服务提供程序API;
- WPU:分层服务提供程序使用的支持函数;
- NSP:命名空间提供程序API。
- 传输服务提供程序
12.1 分层服务提供程序(LSP)
12.1.1 安装LSP
12.1.1.1 安装提供程序条目
12.1.1.2 删除LSP
12.1.1.3 修改LSP条目
12.1.2 编写分层提供程序
12.1.2.1 初始化提供程序
12.1.2.2 创建套接字
12.1.2.3 处理I/O
- 阻塞与非阻塞
- Select和WSPSelect
- WSAAsyncSelect
- WSAEventSelect
- 重叠I/O
12.1.2.4 Winsock扩展函数
12.1.2.5 各种要求
12.1.3 调试LSP
- 危害:开发的LSP出现一个错误,就极有可能使所有访问Winsock位于LSP下边的协议的应用程序崩溃。如果发生在IP上,则注入LSASS之类的关键性服务都会停止。
- 解决:必须重启计算机,进入安全模式,并卸载LSP,就可以将Winsock编录恢复回正常状态。
- 比较好的做法是在重启系统之前对LSP进行调试。
12.1.4 LSP示例
12.2 命名空间服务提供程序
12.2.1 命名空间的安装
12.2.2 命名空间的实现
12.2.3 命名空间提供程序示例
13 使用C#进行.NET套接字编程
14 Visual Basic Winsock控件
15 远程访问服务
15.1 RAS客户机
15.2 编译和链接
15.3 数据结构和平台兼容性问题
15.4 DUN1.3升级和Windows95
15.5 RASDIAL
15.6 电话簿
15.7 连接管理
15.8 VPN
16 IP助手函数
以编程方式获取如下IP实用程序中的可用功能:
- ipconfig.exe
- ipv6.exe
- netstat.exe
- route.exe
- arp.exe