Windows网络编程之Client与Server模型(一)
一、WSAStartup函数
WSAStartup
是 Windows 套接字 API 中的一个函数,它用于初始化 Winsock 库,这是 Windows 中用于网络编程的库。使用 Winsock,您可以创建客户端和服务器应用程序,以便它们能够通过网络进行通信。
函数的原型如下:
1 2 3 4 | int WSAStartup( WORD wVersionRequested, LPWSADATA lpWSAData ); |
参数说明:
wVersionRequested
:指定要使用的 Winsock 版本,通常使用MAKEWORD(majorVersion, minorVersion)
来设置。例如,要使用版本 2.2,您可以使用MAKEWORD(2, 2)
。lpWSAData
:指向WSADATA
结构的指针,用于接收关于 Winsock 初始化的信息。
函数返回值:
- 如果函数成功初始化 Winsock 库,它将返回 0。
- 如果发生错误,它将返回一个错误代码
错误码:
注意:在使用 Winsock 库中的其他函数之前,必须调用 WSAStartup
来初始化库,而且在应用程序退出时调用 WSACleanup
来释放相关资源。
二、socket函数
Windows中,socket()
函数用于创建套接字,并接受一些参数来指定套接字的类型、协议族和选项。
函数的原型如下:
1 2 3 4 5 | SOCKET socket( int af, // 地址族(Address Family) int type, // 套接字类型(Socket Type) int protocol // 协议(Protocol) ); |
参数说明:
af(地址族 - Address Family):
- AF_INET:IPv4 地址族。
- AF_INET6:IPv6 地址族。
- AF_UNIX 或 AF_LOCAL:本地(Unix 域)套接字。
- AF_BLUETOOTH:蓝牙地址族。用于创建蓝牙套接字,用于蓝牙设备之间的通信。
- AF_IRDA:红外线地址族。用于创建红外线套接字,通常用于红外线设备之间的通信。
- AF_SNA:IBM SNA(Systems Network Architecture)地址族。用于创建SNA网络中的套接字。
- AF_DECnet:DECnet地址族。用于创建DECnet网络中的套接字,通常在老式DEC设备之间使用。
type(套接字类型 - Socket Type):
- SOCK_STREAM:一种套接字类型,提供带有OOB数据传输机制的顺序,可靠,双向,基于连接的字节流,此套接字类型使用传输控制协议(TCP)作为Internet地址系列(AF_INET或AF_INET6)。
- SOCK_DGRAM:一种支持数据报的套接字类型,它是固定(通常很小)最大长度的无连接。不可靠的缓冲区,此套接字类型使用用户数据报协议(UDP)作为Internet地址系列(AF_INET或AF_INET6)。
- SOCK_RAW:一种套接字类型,提供允许应用程序操作下一个上层协议头的原始套接字,要操作IPv4标头,必须在套接字上设置IP_HDRINCL套接字选项,要操作IPv6标头,必须在套接字上设置IPV6。
- SOCK_RDM:一种套接字类型,提供可靠的消息数据报,这种类型的一个示例是Windows中的实用通用多播(PGM)多播协议实现,通常称为可靠多播节目,仅在安装了可靠多播协议时才支持此类型值。
- SOCK_SEQPACKET:一种套接字类型,提供基本数据报的伪流数据包。
protocol(协议 - Protocol):
- IPPROTO_TCP:传输控制协议(TCP),当af参数为AF_INET或AF_INET6且类型参数为SOCK_STREAM时,这是一个可能的值
- IPPROTO_UDP:用户数据报协议(UDP),当af参数为AF_INET或AF_INET6且类型参数为SOCK_DGRAM时,这是一个可能的值
- IPPROTO_ICMP:Internet控制消息协议(ICMP),当af参数为AF_UNSPEC,AF_INET或AF_INET6且类型参数为SOCK_RAW或未指定时,这是一个可能的值
- IPPROTO_IGMP:Internet组管理协议(IGMP),当af参数为AF_UNSPEC,AF_INET或AF_INET6且类型参数为SOCK_RAW或未指定时,这是一个可能值
- 0:通常根据 af 和 type 参数自动选择合适的协议。对于 AF_INET 和 SOCK_STREAM,通常会选择 TCP 协议。
返回值说明:
- socket() 函数的返回值是一个 SOCKET 类型的套接字描述符(socket descriptor)。这个描述符是一个整数值,它代表了创建的套接字。如果socket不使用了,需要销毁套接字:closesocket(socketServer);
- 如果没有发生错误,套接字将返回引用新套接字的描述符。否则,返回 INVALID_SOCKET 值,并且可以通过调用 WSAGetLastError 检索特定的错误代码。
三、bind函数
在Windows中,bind()
函数用于将一个套接字绑定到一个特定的本地地址和端口号,以便该套接字可以监听指定的地址和端口或用于连接到远程主机。
函数的原型如下:
1 2 3 4 5 | int bind( SOCKET s, // 套接字描述符 const struct sockaddr* name, // 指定要绑定的地址和端口 int namelen // 地址结构的长度 ); |
参数说明:
- s:要绑定的套接字的描述符,即通过 socket() 函数创建的套接字。
- name:指向包含要绑定的本地地址和端口信息的 sockaddr 结构体的指针。在IPv4中,通常使用 sockaddr_in 结构体。在IPv6中,使用 sockaddr_in6 结构体。
- namelen:指定 name 参数所指向的地址结构的长度,以字节为单位。通常使用 sizeof(struct sockaddr_in) 或 sizeof(struct sockaddr_in6) 来获取长度。
返回值:
- bind() 函数的返回值为整数,通常是一个错误码。如果绑定成功,返回值为0;如果绑定失败,返回值为SOCKET_ERROR,可以使用 WSAGetLastError() 函数获取错误码,然后根据错误码来处理错误情况。
1 2 3 4 5 6 7 8 9 10 11 | struct sockaddr { ushort sa_family; char sa_data[14]; }; struct sockaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; }; |
四、listen()函数
listen()
函数通常用于编程中处理网络套接字,特别是在服务器端编程中。它是一个阻塞函数,用于监听指定的网络地址和端口,以便接受传入的连接请求。一旦有连接请求到达,listen()
将其接受并创建一个新的套接字来处理与客户端的通信。
函数原型:
1 | int listen(SOCKET s, int backlog); |
参数说明:
- s:服务器端的socket,也就是socket函数创建的。
- backlog:指定了操作系统在接受连接请求时可以排队的最大连接数。如果传递的连接请求超过了这个限制,新的连接请求将被拒绝。一般填写SOMXCONN,其作用是让系统自动选择最合适的个数
返回值:
- 成功:返回0,失败:SOCKET_ERROR;具体错误码,由WSAGetLastError()获取。
五、accept函数
accept()
函数用于在服务器套接字上监听传入的连接请求,并在有连接请求到达时创建一个新的套接字,该新套接字用于与客户端进行通信。
函数原型:
1 | SOCKET accept(SOCKET s, struct sockaddr* addr, int * addrlen); |
参数介绍:
s
:这是服务器套接字,用于监听传入连接请求的套接字。addr
:这是一个指向sockaddr
结构的指针,用于存储连接客户端的地址信息。你可以将其设置为NULL
,如果你不关心客户端的地址信息。addrlen
:这是一个指向整数的指针,用于指定addr
缓冲区的大小。通常情况下,你可以将其设置为sizeof(struct sockaddr)
。
返回值:
- 成功:返回值就是给客户端包好的socket,与客户端通信就靠这个;失败:SOCKET_ERROR;具体错误码,由WSAGetLastError()获取。
六、recv函数
recv()
函数是在Windows中用于接收数据的函数,通常与套接字(socket)一起使用。它允许你从一个连接的套接字接收数据。
函数原型:
1 | int recv(SOCKET s, char * buf, int len, int flags); |
参数介绍:
s
:这是要接收数据的套接字。buf
:这是一个指向接收数据的缓冲区的指针。len
:这是要接收的最大字节数。flags
:这是一个标志参数,通常可以设置为0,表示默认行为。
返回值:
- 读出来的字节数大小,如果没有可读数据,recv则会阻塞等待,直到客户端发来数据。
- 如果客户端断开连接,则返回0。
- 执行失败,返回SOCKET_ERROR,根据WSAGetLastError()获取错误码。
七、send()函数
send()
函数是在Windows中用于发送数据的函数,用于将数据从指定的缓冲区发送到已连接的套接字
函数原型:
1 | int send(SOCKET s, const char * buf, int len, int flags); |
参数介绍:
s
:这是要发送数据的套接字。buf
:这是一个指向包含要发送数据的缓冲区的指针。len
:这是要发送的数据的字节数。flags
:这是一个标志参数,通常可以设置为0,表示默认行为。
返回值:
- 成功返回写入的字节数,执行失败,返回SOCKET_ERROR;通过WSAGetLastError获取错误码
八、Server模型完整示例源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | #include <WinSock2.h> #include <stdio.h> #pragma comment(lib, "ws2_32.lib") int main() { WSADATA wsaData; // 创建一个 WSADATA 结构 // 初始化 Winsock 库,指定要使用的版本 int ret = WSAStartup(MAKEWORD(2, 2), &wsaData); if (ret != 0) { printf ( "WSAStartup 失败,错误码: %d\n" , ret); return 0; } //校验版本 if (HIBYTE(wsaData.wVersion) != 2 || LOBYTE(wsaData.wVersion) != 2) { printf ( "版本不符合" ); WSACleanup(); return 0; } // 在这里进行网络编程操作 SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (socketServer == INVALID_SOCKET) { int errorCode = WSAGetLastError(); printf ( "socket创建失败,错误码:%u\n" , errorCode); closesocket(socketServer); WSACleanup(); } sockaddr_in si; si.sin_family = AF_INET; si.sin_addr.s_addr = inet_addr( "127.0.0.1" ); //si.sin_addr.s_addr = INADDR_ANY; // 使用0.0.0.0监听所有可用端口 si.sin_port = htons(1234); ret = bind(socketServer, (SOCKADDR*)&si, sizeof (si)); if (ret == SOCKET_ERROR) { printf ( "bind绑定失败,错误码:%u\n" , WSAGetLastError()); closesocket(socketServer); WSACleanup(); return 0; } ret = listen(socketServer, SOMAXCONN); if (ret == SOCKET_ERROR) { printf ( "listen监听失败,错误码:%u\n" , WSAGetLastError()); closesocket(socketServer); WSACleanup(); return 0; } //创建客户端链接 sockaddr_in siClient; int nLen = sizeof (siClient); SOCKET socketClient = accept(socketServer, (SOCKADDR*)&siClient, &nLen); if (socketClient == INVALID_SOCKET) { printf ( "accept失败,错误码:%u\n" , WSAGetLastError()); closesocket(socketServer); WSACleanup(); return 0; } printf ( "客户端连接成功\n" ); while ( true ) { Sleep(2000); char buffer[1024] = { 0 }; ret = recv(socketClient, buffer, sizeof (buffer), 0); if (ret == 0) { printf ( "客户端连接中断\n" ); } else if (ret == SOCKET_ERROR) { printf ( "recv失败,错误码:%u\n" , WSAGetLastError()); } else { printf ( "recv_len:%d,recv_data:%s\n" , ret, buffer); } const char send_buff[] = "hello, I'm is server" ; ret = send(socketClient, send_buff, strlen (send_buff), 0); if (ret == SOCKET_ERROR) { printf ( "send失败,错误码:%u\n" , WSAGetLastError()); } } closesocket(socketClient); closesocket(socketServer); // 当程序完成网络编程后,确保调用 WSACleanup 来释放 Winsock 资源 WSACleanup(); return 1; } |
九、connect函数
connect
函数通常用于在网络编程中建立客户端与服务器之间的连接。
函数原型:
1 | int connect(SOCKET s, const sockaddr* name, int namelen); |
参数介绍:
s
:要连接的套接字。name
:一个指向sockaddr
结构体的指针,其中包含要连接的目标服务器的地址信息(IP 地址和端口号)。namelen
:name
结构体的长度。
返回值:
connect
函数的返回值表示连接的状态,具体含义如下:
-
如果连接成功,
connect
函数返回值为 0(0
表示成功),表示客户端套接字已成功与服务器建立连接。 -
如果连接失败,
connect
函数返回值为 SOCKET_ERROR(通常定义为-1)
,表示连接尝试失败。此时,可以使用 WSAGetLastError 函数获取详细的错误码来诊断连接失败的原因。
十、Client模型完整示例源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | #include <WinSock2.h> #include <stdio.h> #pragma comment(lib, "ws2_32.lib") int main() { WSADATA wsaData; // 创建一个 WSADATA 结构 // 初始化 Winsock 库,指定要使用的版本 int ret = WSAStartup(MAKEWORD(2, 2), &wsaData); if (ret != 0) { printf ( "WSAStartup 失败,错误码: %d\n" , ret); return 0; } //校验版本 if (HIBYTE(wsaData.wVersion) != 2 || LOBYTE(wsaData.wVersion) != 2) { printf ( "版本不符合" ); WSACleanup(); return 0; } // 在这里进行网络编程操作 SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (socketServer == INVALID_SOCKET) { int errorCode = WSAGetLastError(); printf ( "socket创建失败,错误码:%u\n" , errorCode); closesocket(socketServer); WSACleanup(); } //链接服务器 sockaddr_in si; si.sin_family = AF_INET; si.sin_addr.s_addr = inet_addr( "127.0.0.1" ); si.sin_port = htons(1234); ret = connect(socketServer, (SOCKADDR*)&si, sizeof (si)); if (ret == SOCKET_ERROR) { printf ( "connect失败,错误码:%u\n" , WSAGetLastError()); closesocket(socketServer); WSACleanup(); return 0; } while ( true ) { const char send_buff[] = "hello, I'm is client" ; ret = send(socketServer, send_buff, sizeof (send_buff), 0); if (ret == SOCKET_ERROR) { printf ( "send失败,错误码:%u\n" , WSAGetLastError()); } Sleep(1000); char buffer[1024] = { 0 }; ret = recv(socketServer, buffer, sizeof (buffer), 0); if (ret == 0) { printf ( "客户端连接中断\n" ); } else if (ret == SOCKET_ERROR) { printf ( "recv失败,错误码:%u\n" , WSAGetLastError()); } else { printf ( "recv_len:%d,recv_data:%s\n" , ret, buffer); } } closesocket(socketServer); WSACleanup(); return 1; } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?