本节将逐一介绍WinSock的主要特性和组件,套接字、WinSock动态库的使用。
本节必须掌握的知识点:
Windows Socket接口简介
Windows Socket接口的使用
第178练:网络时间校验
24.1.1 Windows Socket接口简介
■以下是WinSock的主要特性和组件:
●套接字(Socket): 套接字是网络通信的基本概念,它代表了一个网络连接的端点。WinSock提供了函数来创建、绑定、连接和关闭套接字,以及发送和接收数据。
●协议支持: WinSock支持多种网络协议,包括TCP/IP、UDP、IPX/SPX等。开发者可以根据需要选择适当的协议。
●网络地址转换: WinSock提供了用于网络地址转换的函数,使开发者可以将人类可读的IP地址和端口号转换为计算机可理解的格式,并在不同的网络字节序(大端序和小端序)之间进行转换。
●异步操作: WinSock支持异步操作,允许开发者在进行网络通信时使用非阻塞操作,以提高效率和响应性。
●多线程支持: WinSock支持在多线程环境中进行网络编程,可以同时处理多个套接字和连接。
●错误处理: WinSock定义了一套错误码和错误处理函数,使开发者能够检测和处理网络操作中的错误情况。
●使用WinSock进行网络编程时,开发者通常需要按照以下步骤进行操作:
1.初始化WinSock库,通过调用WSAStartup函数来启动WinSock。
2.创建套接字,通过调用socket函数创建一个套接字,并指定协议类型、地址族等参数。
3.配置套接字,通过设置套接字选项,如设置超时时间、启用广播等。
4.绑定套接字到本地地址,通过调用bind函数将套接字与本地IP地址和端口号绑定。
5.连接到远程主机,对于客户端应用程序,通过调用connect函数连接到远程服务器。
6.发送和接收数据,使用send和recv函数发送和接收网络数据。
7.关闭套接字,通过调用closesocket函数关闭套接字,释放资源。
8.清理WinSock库,通过调用WSACleanup函数来终止WinSock。
■TCP/IP模型
●TCP/IP的核心协议运行于传输层和Internet层,主要包括TCP、UDP和IP协议,而TCP协议和UDP协议是以IP协议为基础而封装的。这两种协议提供了不同方式的数据通信服务。
●IP协议比喻为道路,则下一层的网络访问层上的协议相当于不同的铺路材料,上面的TCP和UPD协议相当于路上跑的不同类型的车辆,再上层应用层的协议相当于车上的丰富多彩的货物。他们都是以TCP、UDP为载体的。
图24-1 OSI模型、TCP/IP模型的结构和WinSock接口的关系
■ WinSock动态库
●早期的1.1版的WinSock接口最后也是调用2.0版的WS2_32.dll文件的
●使用前须包含头文件#include Winsock2.h和增加导入库Ws2_32.lib
■加载和释放动态链接库
●WSAStartup函数:WSAStartup(wVersionRequested, lpWSAData)
参数 |
含义 |
WORD wVersionRequested |
指定动态库的版本号。如2.0版时0x0002(MAKEWORD(2,0)) |
LPWSADATA lpWSAData |
指向WSADATA结构体,用来返回动态链接库的详细信息。 wVersion:库文件建议应用程序使用的版本 wHighVersion:库文件支持的最高WinSock版本 szDescription:返回库描述字符串,如“WinSock2.0”之类的。 szSystemStatus:系统状态字符串:返回如“Runing”之类的状态 iMaxSockets:同时支持的最大套接字数量 iMaxUpdDg: 2.0版中己废弃的字段 lpVendorInfo:2.0版中己废弃的字段 |
返回值 |
如果装入成功,返回0。否则,返回出错代码: WSASYSNOTREADY:网络子系统未准备好 WSAVERNOTSUPPORTED:不支持指定的版本 WSAEINPROGRESS::另一个阻塞方式的WinSock1.1操作正在进行中 WSAEPROCLIM:WinSock接口己达到所支持的最大任务数 WSAEFAULT:输入参数lpWSAData指定的指针无效 ★该函数出错时直接返回出错代码,因为库还没装入,无法使用WSAGetLastError函数。其他WinSock函数出错时返回SOCKET_ERROR或INVALID_SOCKET,要进一步得到出错代码,须调用WSAGetLastError函数来获取。 |
●释放WinSock:int WSACleanup(void);//返回值成功为0,否则为SOCKET_ERROR
24.1.2 Windows Socket接口的使用
■ IP地址的转换
●IP地址和端口:
1.IP地址(32位):如11000000.10101000.00000001.01100100(192.168.1.100)
2.端口(16位):即端口数量为65536个。
3.TCP协议和UDP协议是两个完全独立的模块,两者的工作互不相干,所以TCP和UDP各自的端口号也相互独立,即一个进程使用TCP协议的某个端口号并不影响另一进程使用UDP协议的同名端口号。但同一协议的同一端口号无法同时被两个进程同时使用。
●常用协议和应用程序使用的默认端口号
协议或应用程序 |
TCP端口号 |
UDP端口号 |
FTP |
21 |
|
Telnet |
23 |
|
SMTP |
25 |
|
HTTP |
80 |
|
POP3 |
110 |
|
DNS查询 |
53 |
|
TFTP协议 |
69 |
|
NetBIOS名字服务 |
137 |
|
NetBIOS数据包服务 |
138 |
|
SQLServer数据库 |
139、1433 |
|
Oracle数据库 |
1521 |
●sockaddr_in结构体:由于TCP和UDP协议必须同时指定IP和端口号。(封装)
字段 |
含义 |
short sin_family |
地址族,指明互联网的地址类型。在WinSock中必须为AF_INET |
unsigned short sin_port |
端口号(使用网络字节顺序),如53端口号,则等于htons(53); |
struct in_addr sin_addr |
IP地址(使用网络字节顺序),如 sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); sin_addr.S_un.S_addr=htonl(INADDR_ANY); |
char sin_zero[8]; |
空字节 |
■ 网络字节顺序——大端模式
●小端模式:低位放低地址,高位放高地址。如Intel80x86系列的处理器
●大端模式:低位放高地址,高位放低地址。如RISC芯片、网络字节。如0x12345678,则依次送入端口中号的数据为0x12、0x34、0x56、0x78(从低地址开始发送)。
■ 字节顺序转换函数
函数 |
说明 |
htons、htonl |
将16(或32)位的当前主机字节顺序数据转为网络顺序 |
ntohs、ntohl |
将16(或32)位的网络顺序的数据转为当前主机字节顺序 |
inet_addr |
将字符串转为IP地址,如inet_addr(“127.0.0.1”); |
inet_ntoa |
将IP转为字符串。如inet_ntoa(sa.sin_addr) |
■网络应用程序的一般流程
●TCP协议的特征和TCP程序的工作流程
1.特点:面向连接、可靠的字节流服务
面向连接 |
①两个TCP套接字在开始传输数据之前必须先建立一个连接。(犹如打电话) ②TCP协议不能用于广播 |
字节流服务 |
传输入数据是流方式的,没有边界。如发送方分3次发送100、150、200字节的数据包。对接收来说,无法知道数据如何分割,可以一次接收450字节,也可以分10次接收,每次接收45字节。 |
可靠 |
①采用超时及重传机制保证不丢失数据。每发送一个数据包,会启动一定时器,等待对方确认收到这个包。如果指定时间内没得到确认,会重发这个数据包。 ②如果接收方发现数据包校验有错,TCP协议丢弃这个数据包,并且不发送确认,从而发送方因收不到确认而重发这个数据包。 |
2.数据包在传输的时候会通过多个路由器,不同数据包到达终点的先后顺序可能与发送数据包的先后顺序不同。但这没关系,因为TCP协议首部保存数据包的序号。如有必要,在收到数据时TCP协议会重新排序,并将正确的顺序交给应用程序。
3.接收方收到的数据包有可能重复,原因之一是发送和确认之间有个时差,发送方可能因超时而重发数据,对于这种情况,接收方会丢弃重复的数据。
4.TCP协议还提供流量控制机制,发送方可根据接收方应答时间和速率来调整数据的发送速度。防止速度太快,使接收方出现缓冲区溢出。
图24-3 TCP服务器和客户端模型
【注意】客户端无需绑定IP,只需向服务端发起连接请求。服务端处于监听状态。
●UDP协议的特征和UDP程序的工作流程
1.特点:是一个无连接的,面向消息的,不可靠的传输层协议。
无连接 |
①客户端在发送UDP数据包前不需要先与服务器端进行握手确认。无法确认对方是否在线、也无法确认对方指定的端口是否在监听。属于“发出就不管”的协议 ②同一个UDP套接字可以向任何服务器地址发送数据,而无须创建多个套接字,即可采用广播方式。 |
面向消息 |
UDP数据包是有边界保护的。如发送方分三次分别发送100、150、200字节的UDP数据包,接收方必须分三次接收这些数据包。各个数据包之间的数据不会粘连。 |
不可靠 |
UDP协议并不对数据的可靠性与有序性等进行控制。 |
2.TCP协议像打电话,而UDP协议像寄信。发信人虽然知道收信人的地址,但他并确定信是否会被收到。如果发了好几封信,在收信人回信之前,发信人也无法确定信件是否安全、无损和有序的到达。
3.UDP协议不对数据进行可靠性保证,因此传输的效率较高。经常用在在线视频的传送。
图24-4 UDP服务器和客户端模型
【注意】客户端不必连接,直接发送数据。服务端也不必进入监听状态。客户端与服务端的唯一区别就是服务端必须首先将套接字绑定到一个固定端口。以便客户端能向约定的端口发送数据。
■套接字
在Windows操作系统中,套接字(Socket)是用于进行网络通信的一种机制,它提供了一种在网络上发送和接收数据的方式。Windows提供了一套名为Winsock的API(应用程序编程接口),用于在Windows平台上进行套接字编程。
Winsock是Windows Sockets的缩写,它是对底层网络协议的封装和抽象,使得开发者可以使用统一的接口进行网络编程,而无需关注底层协议的细节。
●在Windows中使用套接字进行网络编程时,通常遵循以下步骤:
1.初始化Winsock库:使用WSAStartup函数初始化Winsock库,这是进行任何套接字操作之前必须执行的步骤。
2.创建套接字:使用socket函数创建一个套接字,指定协议类型(如TCP或UDP)和其他参数。
3.绑定套接字:如果是服务器端程序,可以使用bind函数将套接字绑定到特定的IP地址和端口号。
4.监听连接请求:如果是服务器端程序,使用listen函数开始监听连接请求。
5.接受连接:使用accept函数接受客户端的连接请求,建立与客户端的连接。
6.连接到远程主机:如果是客户端程序,使用connect函数连接到远程服务器。
7.发送和接收数据:使用send和recv函数发送和接收数据。对于TCP套接字,可以进行可靠的、面向连接的数据传输;对于UDP套接字,可以进行不可靠的、无连接的数据传输。
8.关闭套接字:使用closesocket函数关闭套接字,释放资源。
9.清理Winsock库:使用WSACleanup函数在程序结束时清理Winsock库。
Winsock API提供了一系列函数和数据结构,用于管理套接字和进行网络通信操作。这些函数包括socket、bind、listen、accept、connect、send、recv、closesocket等。通过使用这些函数,开发者可以方便地进行网络编程,并实现各种网络通信需求。
套接字建立用来通信的对象,是“通信的一端”。套接字的种类:流套接字(stream socket)、数据报套接字(datagram socket)、原始套接字(raw socket)、可靠信息分递套接字(rdm socket)、连续小分包套接字(seqpacket socket)。
●套接字的创建和关闭
1.创建套接字:SOCKET socket(af, type, protocol)
参数 |
含义 |
int af |
用来指定套接字使用的地址格式,和sockaddr_in中的sin_family的定义是一样的。唯一可使用的值是AF_INET。 |
int type |
用来指定套接字的类型 SOCK_STREAM——流套接字,使用TCP协议提供有连接和可靠的传输 SOCK_DGRAM——数据报套接字,使用UDP协义提供无连接的不可靠的传输 SOCK_RAW——原始套接字,WinSock接口并不使用某种特定的协议去封装它,而是由程序自行处理数据包,以及协议首部,正因为如此,所以可以使用特殊的功能,如伪造发送者地址等。 |
int protocol |
当type指定为SOCK_RAW时,protocol可指定以下的值 ①IPPROTO_IP、IPPROTO_ICMP、IPPROTO_TCP、IPPROTO_UDP:分别指定使用IP、ICMP、TCP和UDP协议。这时会自动为数据加上IP首部。并且将IP首部中的上层协议字段设置为指定的这些协议的名称。但是使用这个套接字接收数据时,系统却不会将IP首部自动去除,需要自行处理。 ②IPPROTO_RAW:系统将数据包直接送到网络访问层,程序需要自己添加IP首部及其他协议的首部,并用需要自己计算和填充协议首部中的检验和字段。但这个socket只能用来发送数据包而无法接收数据。 |
2.关闭套接字:int closesocket(SOCKET s);
●套接字的工作模式:阻塞模式(创建时默认的工作方式)和非阻塞模式。
■监听、发起连接和接收连接
●TCP客户端——连接到服务器:connect函数
参数 |
含义 |
SOCKET s |
TCP套接字的句柄 |
const struct sockaddr FAR *name |
指向一个sockaddr_in结构,用来指定服务器端的地址和端口 |
int namelen |
指定的sockaddr_in结构的长度 |
返回值 |
①阻塞模式下:成功返回0,否则SOCKET_ERROR。要知道详细原因,可调用WSAGetLastError函数。 常见错误: WSAECOONNERREFUSED:服务器没有在指定端口监听。 WSA_ETIMEDOUT:网络不通,或服务器不在线 ②非阻塞模式:均返回SOCKET_ERROR,但并不意味着连接失败,而是指函数返回里连接尚末成功。要调用WSAGetLastError得到出错代码。只是WSAEWOULDBLOCK才表示连接失败。 |
【注意】客户端发起连接时,系统会自动为套接字选择一个空闲的端口,如果一定要用特定的端口连接服务器,可在调用connect前用bind函数来指定端口。
●TCP服务器端——在指定的IP地址和端口监听并接收连接
1.绑定IP和端口:int bind(SOCKET s, const struct sockaddr FAR *name,int namelen );
参数 |
含义 |
SOCKET s |
TCP套接字的句柄 |
const struct sockaddr FAR *name |
指向一个sockaddr_in结构,用来指定需要绑定的服务器端的地址和端口。sin_addr字段的设置: INADDR_ARRAY(0):自动在本机的所有IP地址上监听 //如本机有3个网卡,配置3个IP //那么会自动在监听3个地址上监听 指定为内网IP:在指定的那个地址上进行监听 |
int namelen |
指定的sockaddr_in结构的长度 |
返回值 |
绑定成功,返回0。否则返回SOCKET_ERROR。一般是端口是被其他程序占用,出错代码为WSAEADDRINUSE。如果套接字己经绑定过了,返回WSAEFAULT。 |
2.监听:int listen(SOCKET s, int backlog);
参数 |
含义 |
SOCKET s |
TCP套接字的句柄 |
int backlog |
监听队列中允许保持的尚未处理的最大连接数量。当套接字监听到客户端连接请求时,还需要调用accept才能建立真正的连接。在调用accept之前,连接请求会被保留在队列 中,如果这时另一个客户端也发起连接的话,这个连接也会被保留在队列里。Backlog指的就是这个队列最大的长度。 |
返回值 |
成功,返回0。这里套接字处于等待连接进入的状态。失败返回SOCKET_ERROR。如果没有bind操作就去listen,这里的出错代码是WSAINVAL。 |
3.接受连接:SOCKET accept(SOCKET s, struct sockaddr FAR *addr,int FAR *addrlen);
参数 |
含义 |
SOCKET s |
监听中的套接字句柄 |
struct sockaddr FAR *addr |
addr指向一个缓冲区,函数会在这里返回一个sockaddr_in结构。结构中存放有连接请求方的IP地址和端口(即客户端的IP和端口)。可以通过这个参数,对客户端进行认证,如果检测到IP不合法,则调用closesocket关闭这个新套接字。如果不需要得到对方的地址信息,addr和addrlen都设为NULL。 |
addrlen |
指向一个int型的变量,函数在这里放入返回到上述结构长度。 |
返回值 |
如果成功,函数新建一个TCP套接字,这个新的套接字才是用来和该客户端连接。原来的套接字仍保持着监听状态。当要断开与客户端的连接时,也是要对这个新的套接字调用closesocket。 如果失败,返回INVALID_SOCKET。 |
●典型的accept处理
while(TRUE)
{
SOCKET sc=accept(hListenSocket,NULL,0);
if (sc==INVALID_SOCKET) break;
//在这里创建一个新线程,对新套接字进行通信,以便马上能处理新连接。
//但这里直接对新连接进行数据收发,因为其他客户的连接请求可能没办法及时处理。
//新套接字可以通过lParam参数传递线程函数。
//可以用其他线程中closesocket这个监听套接字,表示不再进行监听。这样accept
//会返回INVALID_SOCKET,这样程序就可以退出循环。(注意关闭的是监听套接字)
}
■数据的收发
TCP一旦连接(对客户端来说是connect返回成功,对服务器端来说是accept返回新套接字)。那么连接双方是对等的,因为TCP连接是一个全双工的连接。任