Winsock 编程

目录

  1. 通用函数讲解
    1. WSAStartup
    2. WSACleanup
    3. socket
    4. closesocket
  2. 面向连接的函数讲解
    1. bind
    2. listen
    3. accept
    4. connect
    5. send
    6. recv
  3. 面向非连接的函数讲解
    1. sendto
    2. recvfrom
  4. 字节顺序
  5. 获取本地 ip 填充 sockaddr_in
    1. gethostname
    2. gethostbyname
    3. 获取本地 ip 填充 sockaddr_in
  6. 入门 TCP/UDP 编程
    1. TCP 基本编程
    2. UDP 基本编程
    3. TCP 封装
    4. UDP 封装
  7. 借助多线程实现 TCP 全双工通信
  8. TCP 多连接多请求设计
  9. Winsock 其他函数
    1. getsockname
    2. setsockopt
    3. getsockopt
    4. shutdown
    5. ioctlsocket
    6. WSAAsyncSelect

一、通用函数讲解

  • 摘要:WSAStartup / WSACleanup / socket / closesocket

1、WSAStartup

  • WSA:即 Windows Sockets Asynchronous,Windows 异步套接字

  • 作用:WSAStartup 必须是应用程序或 DLL 调用的第一个 Windows Sockets 函数。它允许应用程序或 DLL 指明Windows Sockets API 的版本号及获得特定 Windows Sockets 实现的细节。应用程序或 DLL 只能在一次成功的 WSAStartup() 调用之后才能调用进一步的 Windows Sockets API 函数

  • 函数原型:

    int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
    
  • 参数:

    • wVersionRequested:一个 WORD(双字节)型数值,在最高版本的 Windows Sockets 支持调用者使用,高阶字节指定小版本(修订本)号,低位字节指定主版本号。当前常用的 Winsock sockets 的版本号为 2.2,用到的 DLL 是 ws2_32.dll,使用 MAKEWORD(a, b) 生成

    • lpWSAData:指向 WSADATA 数据结构的指针,用来接收 Windows Sockets 实现的细节

  • 返回值:

    • 0 成功
    • 否则返回相关错误代码
    • 所有的 Winsock 函数出错后,都可以调用 WSAGetLastError() 函数得到错误码,但是 WSAStartup()
      不能通过 WSAGetLastError() 得到错误码,因为 WSAStartup() 未调用成功,不能调用 WSAGetLastError() 函数
  • 一些错误代码:

    • WSAVERNOTSUPPORTED:值为 10092,表示所需的 Windows Sockets API 的版本未由特定的Windows Sockets 实现提供
    • WSANOTINITIALISED:值为 10093,在使用此 API 之前应首先成功地调用 WSAStartup()
    • WSAEPROTONOSUPPORT:值为 10043,不支持指定的协议
    • 这里没法展示所有错误码,通过 WSAGetLastError() 获得错误码,通过任意错误码可转到源码中查看相关错误码代表内容(如 VS 中右击 WSANOTINITIALISED,速览定义或转到定义可查看源码)
  • 对于 WSAStartup() / WSACleanup() 和 socket() / closesocket() 这样的函数,最好保持成对出现

  • 测试:

    WSADATA wsaData;
    cout << WSAStartup(MAKEWORD(2, 2), &wsaData) << endl;		// 0
    cout << WSAStartup(MAKEWORD(512, 512), &wsaData) << endl;	// 10092
    

2、WSACleanup

  • 作用:用于终止 ws2_32.dll 的使用

  • 函数原型:

    int WSACleanup();
    
  • 返回值:

    • 0 成功
    • 否则返回值为 SOCKET_ERROR,可以通过调用 WSAGetLastError() 获取错误代码
    • SOCKET_ERROR 值为 -1
  • 测试:

    int main()
    {
    	cout << WSACleanup() << endl;		// -1
    	cout << WSAGetLastError() << endl;	// 10093
    	return 0;
    }
    
    int main()
    {
    	WSADATA wsaData;
    	cout << WSAStartup(MAKEWORD(2, 2), &wsaData) << endl;	// 0
    	cout << WSACleanup() << endl;		// 0
    	cout << WSAGetLastError() << endl;	// 0
    	return 0;
    }
    

3、socket

  • 作用:socket() 函数用于根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源,创建一个套接口

  • 函数原型:

    SOCKET socket(int af, int type, int protocol);
    
  • 参数:

    • af:一个地址描述,指明地址簇类型,在 Windows 下可以使用的参数值有多个,但是真正可以使用的只有两个:AF_INET、PF_INET,这两个宏在 Winsock2.h 下的定义是相同的;AF 表示地址族(Address Family),PF 表示协议族(Protocol Family);一般情况下,在调用 socket() 函数时应该使用 PF_INET,而在设置地址时使用 AF_INET,调用 socket()函数时,应该使用 PF_INET 宏,而尽量避免或不去使用 AF_INET 宏

      #define AF_INET  2	// internetwork: UDP, TCP, etc.
      
      /*
       * Protocol families, same as address families for now.
       */
      #define PF_INET  AF_INET
      
    • type:指定新套接字描述符的类型(指定 socket 类型)。常用的有:

      /*
       * Types
       */
      #define SOCK_STREAM     1		/* stream socket */
      #define SOCK_DGRAM      2		/* datagram socket */
      #define SOCK_RAW        3		/* raw-protocol interface */
      #define SOCK_RDM        4		/* reliably-delivered message */
      #define SOCK_SEQPACKET  5		/* sequenced packet stream */
      
      • SOCK_STREAM:流套接字,用于 TCP 协议

      • SOCK_DGRAM:数据报套接字,用于 UDP 协议

      • SOCK_RAW:原始套接字

    • protocol:指定应用程序所使用的通信协议,该参数取决于 af 和 type 参数的类型。常用的有:

      • IPPROTO_TCP:指明 TCP 协议,af = PF_INET,type = SOCK_STREAM
      • IPPROTO_UDP:指明 UDP 协议,af = PF_INET,type = SOCK_DGRAM
    • 返回值:

      • 若无错误发生,socket() 返回引用新套接口的描述字

      • 否则的话,返回 INVALID_SOCKET 错误,应用程序可通过 WSAGetLastError() 获取相应错误代码

      • 测试:

        SOCKET socketServer;
        socketServer = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
        cout << (socketServer == INVALID_SOCKET) << endl;	// 0
        cout << WSAGetLastError() << endl;					// 0
        
        SOCKET socketServer;
        socketServer = socket(PF_INET, SOCK_STREAM, IPPROTO_UDP);
        cout << (socketServer == INVALID_SOCKET) << endl;	// 1
        cout << WSAGetLastError() << endl;					// 10043
        

4、closesocket

  • 作用:用于关闭一个套接口。更确切地说,它释放套接口描述字 s,以后对 s 的访问均以 WSAENOTSOCK 错误返回

  • 函数原型:

    int closesocket(SOCKET s);
    
  • 参数:

    • s:待关闭的套接字
  • 返回值:

    • 0 成功
    • 否则的话,返回 SOCKET_ERROR 错误,应用程序可通过 WSAGetLastError() 获取相应错误代码
  • 测试:

    cout << closesocket(socketServer) << endl;	// 0
    cout << closesocket(socketServer) << endl;	// -1
    

二、面向连接的函数讲解

  • 摘要:bind / listen / accept / connect / send / recv

1、bind

  • 作用:bind 将一本地地址与一套接口捆绑。适用于未连接的数据报或流类套接口,在 connect() 或 listen() 调用前使用。当用 socket() 创建套接口后,它便存在于一个名字空间(地址族)中,但并未赋名。bind() 函数通过给一个未命名套接口分配一个本地名字来为套接口建立本地捆绑(主机地址/端口号)

  • 函数原型:

    int bind(SOCKET s, const struct sockaddr* name, int namelen);
    
  • 参数:

    • s:指定一个待绑定的 socket,由 socket() 函数创建
    • name:是一个指向 sockaddr 结构体的指针,稍后进行讲解
    • namelen:参数 name 指向对象的字节数,可通过 sizeof() 得到
  • 返回值:

    • 0 成功
    • 否则返回 SOCKET_ERROR,应用程序可通过 WSAGetLastError() 获取相应错误代码
  • 再看参数 name:

    • 结构体 sockaddr 共 16 字节:

      struct sockaddr
      {
      	u_short		sa_family;
      	CHAR		sa_data[14];
      };
      
    • 在该结构体之前所使用的结构体为 sockaddr_in:

      struct sockaddr_in
      {
      	short		sin_family;		// 地址族,常为 AF_INET
      	USHORT		sin_port;		// 端口号,要求大端模式
      	IN_ADDR		sin_addr;		// ip 地址
      	CHAR		sin_zero[8];
      };
      
    • sockaddr 结构体是为了保持各个特定协议之间的结构体兼容性而设计的。为 bind() 函数指定地址和端口时,应向 sockaddr_in 结构体填充相应的内容,而调用函数时应该使用 sockaddr 结构体

    • 在 sockaddr_in 结构体中,还有一个结构体 in_addr,用来表示 IP 地址:

      typedef struct in_addr {
      		union {
      			struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
      			struct { USHORT s_w1,s_w2; } S_un_w;
      			ULONG	S_addr;
      		} S_un;
      #define s_addr	S_un.S_addr	/* can be used for most tcp & ip code */
      #define s_host	S_un.S_un_b.s_b2	// host on imp
      #define s_net	S_un.S_un_b.s_b1	// network
      #define s_imp	S_un.S_un_w.s_w2	// imp
      #define s_impno	S_un.S_un_b.s_b4	// imp #
      #define s_lh	S_un.S_un_b.s_b3	// logical host
      } IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;
      
      • 该结构体中是一个共用体 S_un,包含两个结构体变量和 1 个 u_long 类型变量,

      • 一般使用的 IP 地址是使用点分十进制表示的,而 in_addr 结构体中却没有提供用来保存点分十进制
        表示 IP 地址的数据类型,这时需要使用转换函数,把点分十进制表示的 IP 地址转换成 in_addr 结构体可以接受的类型。这里使用的转换函数是 inet_addr():

        unsigned long inet_addr(const char* cp);
        
        // 上面函数可能被否定,推荐使用 inet_pton() 函数
        // 头文件 <WS2tcpip.h>
        INT inet_pton(
        	INT Family,				// 地址族,常为 AF_INET
        	PCSTR pszAddrString,	// 点分 ip 地址字符串,如 "127.0.0.1"
        	PVOID pAddrBuf			// 指向 sockaddr_in::sin_addr
        );
        // 返回 1 成功,0 失败
        
      • 该函数是将点分十进制表示 IP 地址转换成 unsigned long 类型的数值。该函数的参数 cp 是指向点分十进制 IP 地址的字符指针。同时该函数有一个逆函数,是将 unsigned long 型的数值型 IP 地址转换为点分十进制的 IP 地址字符串,该函数的定义如下:

        char* inet_ntoa(in_addr in);
        
        // 上面函数可能被否定,推荐使用 inet_pton() 函数
        // 头文件 <WS2tcpip.h>
        PCSTR inet_ntop(
        	INT Family,				// 地址族,常为 AF_INET
        	const VOID* pAddr,		// 指向 sockaddr_in::sin_addr
        	PSTR pStringBuf,		// 用于存转换后的点分 ip 地址字符串,如 "127.0.0.1"
        	size_t StringBufSize	// 缓冲空间大小
        );
        // 返回转换后的点分 ip 地址字符串
        
    • sockaddr_in 结构体中的 sin_port 表示端口,这个端口需要使用大端模式字节序存储(之后再介绍大端模式和小端模式字节顺序),在 Intel X86 架构下,数值存储方式默认都是小端模式字节序,而 TCP/IP 的数值存储方式都是大端模式的字节序,为了实现方便的转换,WinSock2.h 中提供了方便的函数,即 htons() 和 htonl() 两个函数,并且提供了它们的逆函数 ntohs() 和 ntohl():

      // htons()
      u_short htons(u_short hostshort);
      // htonl()
      u_long htons(u_long hostshort);
      // ntohs
      u_short ntohs(u_short hostshort);
      // ntohl()
      u_long ntohl(u_long hostshort);
      
    • ntoh 指 network to host,hton 指 host to network

  • 具体 bind 的使用方法:

    sockaddr_in serverAddr;				// sockaddr_in 结构
    serverAddr.sin_family = AF_INET;	// 地址族
    serverAddr.sin_port = htons(8082);	// 端口号,使用 htons 转换到大端模式
    serverAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");	// 设置 ip
    // 绑定
    bind(socketServer, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
    
    // 安全版本
    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(8082);
    inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);	// inet_pton()
    bind(socketServer, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
    

2、listen

  • 作用:当套接字与地址端口信息绑定以后,就需要让端口进行监听,当端口处于监听状态以后就可以接受其他主机的连接了。监听端口和接受连接请求的函数分别为 listen() 和 accept()

  • 函数原型:

    int listen(SOCKET s, int backlog);
    
  • 参数:

    • s:指定要监听的套接字描述符

    • backlog:允许进入请求连接队列的个数,backlog 的最大值由系统指定,在 winsock2.h 中,其最大值
      由 SOMAXCONN 表示:

      #define SOMAXCONN 0x7fffffff
      
  • 返回值:

    • 0 成功
    • 否则返回 SOCKET_ERROR,应用程序可通过 WSAGetLastError() 获取相应错误代码

3、accept

  • 作用:该函数从连接请求队列中获得连接信息,创建新的套接字描述符,获取客户端地址。新创建的套接字用于和客户端进行通信,在服务器和客户端通信完成后,该套接字也需要使用 closesocket() 函数进行关闭,以释放相应的资源

  • 函数原型:

    SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen);
    
  • 参数:

    • s:处于监听的套接字描述符
    • addr:指向 sockaddr 结构体的指针,用来返回客户端的地址信息
    • addrlen:指向 int 型的指针变量,用来传入 sockaddr 结构体的大小
  • 返回值:

    • 如果不发生错误,accept 将返回一个新的 SOCKET 描述符,即新建连接的 socket 句柄
    • 否则,将返回 INVALID_SOCKET,应用程序可通过 WSAGetLastError() 获取相应错误代码
  • 测试:

    sockaddr_in clientAddr;
    int nSize = sizeof(clientAddr);
    SOCKET socketClient = accept(socketServer, (SOCKADDR*)&clientAddr, &nSize);
    cout << (socketClient == INVALID_SOCKET) << endl;	// 0
    
    sockaddr_in clientAddr;
    int nSize = 1;
    SOCKET socketClient = accept(socketServer, (SOCKADDR*)&clientAddr, &nSize);
    cout << (socketClient == INVALID_SOCKET) << endl;	// 1
    

4、connect

  • 客户端请求服务端连接

  • 函数原型:

    int connect(SOCKET s, const struct sockaddr* name, int namelen);
    
  • 参数:

    • s:待连接套接字,指服务端套接字
    • name:指明连接地址,指向服务端 sockaddr
    • namelen:指定 sockaddr 结构体的长度,可通过 sizeof() 得到
  • 返回值:

    • 0 成功
    • 否则返回 SOCKET_ERROR,应用程序可通过 WSAGetLastError() 获取相应错误代码
  • 该函数类似 bind

5、send

  • 作用:发送数据

  • 函数原型:

    int send(SOCKET s, const char* buf, int len, int flags);
    
  • 参数:

    • s:套接字描述符,该套接字描述符对于服务器端而言,使用的是 accept() 函数返回的套接字描述符(客户端套接字),对于客户端而言,使用的是 socket() 函数创建的套接字描述符(服务端套接字)
    • buf:发送消息的缓冲区
    • len:缓冲区的长度
    • flags:通常赋为 0 值
  • 返回值:

    • 无错:返回发送字节数,可能小于 len
    • 出错:返回 SOCKET_ERROR,可通过 WSAGetLastError() 获取错误码

6、recv

  • 作用:接收数据

  • 函数原型:

    int recv(SOCKET s, char* buf, int len, int flags);
    
  • 参数:

    • s:套接字描述符,该套接字描述符对于服务器端而言,使用的是 accept() 函数返回的套接字描述符(客户端套接字),对于客户端而言,使用的是 socket() 函数创建的套接字描述符(服务端套接字)
    • buf:发送消息的缓冲区
    • len:缓冲区的长度
    • flags:通常赋为 0 值
  • 返回值:

    • 正常:返回已接收数据长度
    • 出错:返回 SOCKET_ERROR,可通过 WSAGetLastError() 获取错误码
    • 如果接收缓冲区没有数据可接收,则 recv 将阻塞直至有数据到达

三、面向非连接的函数讲解

  • 摘要:sendto / recvfrom
  • 面向非连接的协议 UDP 服务端不需要 listen & accept,客户端不需要 connet

1、sendto

  • 作用:指向一指定目的地发送数据,sendto() 适用于发送未建立连接的 UDP 数据包

  • 函数原型:

    int sendto(
    	SOCKET s,
    	const char* buf,
    	int len,
    	int flags,
    	const struct sockaddr* to,
    	int tolen
    );
    
  • 参数:

    • s:自身套接字
    • buf:发送消息的缓冲区
    • len:缓冲区的长度
    • flags:通常赋为 0 值
    • to:指向 sockaddr 结构体的指针,对服务端而言是客户端地址,对客户端而言是服务端地址
    • tolen:sockaddr 结构体大小,可通过 sizeof() 获得
  • 返回值:

    • 成功则返回实际传送出去的字符数
    • 失败返回 SOCKET_ERROR,可通过 WSAGetLastError() 获取错误码

2、recvfrom

  • 作用:接收一个数据报并保存源地址

  • 函数原型:

    int recvfrom(
    	SOCKET s,
    	char* buf,
    	int len,
    	int flags,
    	struct sockaddr* from,
    	int* fromlen
    );
    
  • 参数:

    • s:自身套接字
    • buf:接收消息的缓冲区
    • len:缓冲区的长度
    • flags:通常赋为 0 值
    • from:指向 sockaddr 结构体的指针,对服务端而言是客户端地址,对客户端而言是服务端地址
    • fromlen:指向 sockaddr 结构体大小的整形指针
  • 返回值:

    • 正常:返回已接收数据长度
    • 出错:返回 SOCKET_ERROR,可通过 WSAGetLastError() 获取错误码
    • 如果接收缓冲区没有数据可接收,则 recv 将阻塞直至有数据到达

四、字节顺序

  • 通常情况下,数值在内存中存储的方式有两种,一种是大端方式(大端方式字序就是网络字节序),另一种是小端方式

  • 大端方式:高位地址存放低位数据,低位地址存放高位数据

  • 小端方式:高位地址存放高位数据,低位地址存放低位数据

  • 示例(验证本机的字节顺序):

    int val = 0x01020304;
    const char* p = reinterpret_cast<char*>(&val);
    for (int i = 0; i < 4; ++i)
    {
    	cout << "0x" << (void*)(p + i) << " -- " << static_cast<int>(p[i]) << endl;
    }
    // 输出
    0x0093FD1C -- 4
    0x0093FD1D -- 3
    0x0093FD1E -- 2
    0x0093FD1F -- 1
    
    • 可见,高位地址存放高位数据,低位地址存放低位数据,该机器是小端方式字节顺序
  • 网络字节序是大端方式

五、获取本地 ip 填充 sockaddr_in

1、gethostname

  • 作用:该函数把本地主机名存放入由 name 参数指定的缓冲区中。返回的主机名是一个以 NULL 结束的字符串。主机名的形式取决于 Windows Sockets 实现:它可能是一个简单的主机名,或者是一个域名。然而,返回的名字必定可以在 gethostbyname() 和 WSAAsyncGetHostByName() 中使用

  • 函数原型:

    int gethostname(char* name, int namelen);
    
  • 参数:

    • name:接收缓冲区
    • namelen:缓冲区大小
  • 返回值:

    • 0 成功
    • 否则返回 SOCKET_ERROR,应用程序可以通过 WSAGetLastError() 来得到一个特定的错误代码

2、gethostbyname

  • gethostbyname() 返回对应于给定主机名的包含主机名字和地址信息的 hostent 结构的指针

  • 函数原型:

    struct hostent* gethostbyname(const char* name);
    
  • 参数:

    • name:指向主机名的指针,可由 gethostname 获得
  • 返回值:

    • 如果没有错误发生,gethostbyname() 返回如上所述的一个指向 hostent 结构的指针
    • 否则,返回一个空指针。应用程序可以通过 WSAGetLastError() 来得到一个特定的错误代码
  • hostent:

    struct hostent {
    char*   h_name;					/* official name of host */
    char**  h_aliases;				/* alias list */
    short   h_addrtype;				/* host address type */
    short   h_length;				/* length of address */
    char**  h_addr_list;			/* list of addresses */
    #define h_addr h_addr_list[0]	/* address, for backward compat */
    };
    

3、获取本地 ip 填充 sockaddr_in

  • gethostbyname 可能被否定,使用宏消除警告:#define _WINSOCK_DEPRECATED_NO_WARNINGS

    char hostname[MAXBYTE];
    gethostname(hostname, MAXBYTE);
    
    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(8082);
    serverAddr.sin_addr.S_un.S_addr = ((struct in_addr*)gethostbyname(hostname)->h_addr)->s_addr;
    

六、入门 TCP/UDP 编程

1、TCP 基本编程

/*
 * TCP 服务端程序
*/
#include <iostream>
using namespace std;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")

/* WSAStartup()->socket()->bind()->listen()->accept()->send()/recv()->closesocket()->WSACleanup() */

int main()
{
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) return WSAGetLastError();

	SOCKET socketServer;
	socketServer = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (socketServer == INVALID_SOCKET) return WSAGetLastError();

	sockaddr_in serverAddr;
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(8082);
	inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);

	if (bind(socketServer, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) != 0) return WSAGetLastError();
	if (listen(socketServer, 1) != 0) return WSAGetLastError();

	sockaddr_in clientAddr;
	int nSize = sizeof(clientAddr);
	SOCKET socketClient = accept(socketServer, (SOCKADDR*)&clientAddr, &nSize);
	if (socketClient == INVALID_SOCKET) return WSAGetLastError();

	char ipbuf[20];
	cout << "客户端成功接入,ip - port: " << inet_ntop(AF_INET, &clientAddr.sin_addr, ipbuf, 20) << " - " << ntohs(clientAddr.sin_port) << endl;

	cout << "send: server send" << endl;
	cout << "send ret: " << send(socketClient, "server send", strlen("server send") + 1, 0) << endl;
	char buf[256];
	cout << "recv ret: " << recv(socketClient, buf, 256, 0) << endl;
	cout << "recv: " << buf << endl;

	closesocket(socketClient);
	closesocket(socketServer);
	WSACleanup();
	return 0;
}


/*
 * TCP 客户端程序
*/
#include <iostream>
using namespace std;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")

/* WSAStartup()->socket()->connet()->send()/recv()->closesocket()->WSACleanup() */

int main()
{
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) return WSAGetLastError();

	SOCKET socketServer;
	socketServer = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (socketServer == INVALID_SOCKET) return WSAGetLastError();

	sockaddr_in serverAddr;
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(8082);
	inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);

	if (connect(socketServer, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) != 0) return WSAGetLastError();

	char buf[256];
	cout << "recv ret: " << recv(socketServer, buf, 256, 0) << endl;
	cout << "recv: " << buf << endl;
	cout << "send: client send" << endl;
	cout << "send ret: " << send(socketServer, "client send", strlen("client send") + 1, 0) << endl;

	closesocket(socketServer);
	WSACleanup();
	return 0;
}
/*
 * TCP 服务端程序输出
*/
客户端成功接入,ip - port: 127.0.0.1 - 49291
send: server send
send ret: 12
recv ret: 12
recv: client send

/*
 * TCP 客户端程序输出
*/
recv ret: 12
recv: server send
send: client send
send ret: 12

2、UDP 基本编程

/*
 * UDP 服务端程序
*/
#include <iostream>
using namespace std;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")

/* WSAStartup()->socket()->bind()->sendto()/recvfrom()->closesocket()->WSACleanup() */

int main()
{
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) return WSAGetLastError();

	SOCKET mySocket;
	mySocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (mySocket == INVALID_SOCKET) return WSAGetLastError();

	sockaddr_in serverAddr;
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(8082);
	inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);

	if (bind(mySocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) != 0) return WSAGetLastError();

	sockaddr_in clientAddr;
	int nSize = sizeof(clientAddr);

	char buf[256];
	cout << "recvfrom ret: " << recvfrom(mySocket, buf, 256, 0, (SOCKADDR*)&clientAddr, &nSize) << endl;
	cout << "recvfrom: " << buf << endl;

	char ipbuf[20];
	cout << "客户端成功接入,ip - port: " << inet_ntop(AF_INET, &clientAddr.sin_addr, ipbuf, 20) << " - " << ntohs(clientAddr.sin_port) << endl;

	cout << "sendto: server send" << endl;
	cout << "sendto ret: " << sendto(mySocket, "server send", strlen("server send") + 1, 0, (SOCKADDR*)&clientAddr, sizeof(clientAddr)) << endl;

	closesocket(mySocket);
	WSACleanup();
	return 0;
}


/*
 * UDP 客户端程序
*/
#include <iostream>
using namespace std;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")

/* WSAStartup()->socket()->sendto()/recvfrom()->closesocket()->WSACleanup() */

int main()
{
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) return WSAGetLastError();

	SOCKET mySocket;
	mySocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (mySocket == INVALID_SOCKET) return WSAGetLastError();

	sockaddr_in serverAddr;
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(8082);
	inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
	int nSize = sizeof(serverAddr);

	cout << "sendto: server send" << endl;
	cout << "sendto ret: " << sendto(mySocket, "server send", strlen("server send") + 1, 0, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) << endl;
	char buf[256];
	cout << "recvfrom ret: " << recvfrom(mySocket, buf, 256, 0, (SOCKADDR*)&serverAddr, &nSize) << endl;
	cout << "recvfrom: " << buf << endl;

	closesocket(mySocket);
	WSACleanup();
	return 0;
}
/*
 * UDP 服务端程序输出
*/
recvfrom ret: 12
recvfrom: server send
客户端成功接入,ip - port: 127.0.0.1 - 59939
sendto: server send
sendto ret: 12

/*
 * UDP 客户端程序输出
*/
sendto: server send
sendto ret: 12
recvfrom ret: 12
recvfrom: server send

3、TCP 封装

/*
 * TCP 服务端程序
*/
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>

using std::cout;
using std::endl;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")
#include <string>

/* WSAStartup()->socket()->bind()->listen()->accept()->send()/recv()->closesocket()->WSACleanup() */

class TCPServer
{
public:
	TCPServer()
	{
		if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) exit(EXIT_FAILURE);

		socketServer = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
		if (socketServer == INVALID_SOCKET)
		{
			auto code = WSAGetLastError();
			WSACleanup();
			exit(code);
		}

		char hostname[MAXBYTE];
		gethostname(hostname, MAXBYTE);
		serverAddr.sin_family = AF_INET;
		serverAddr.sin_port = htons(8082);
		serverAddr.sin_addr.S_un.S_addr = ((struct in_addr*)gethostbyname(hostname)->h_addr)->s_addr;

		if (bind(socketServer, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) != 0) clear_and_exit();
		if (listen(socketServer, 1) != 0) clear_and_exit();
	}

	~TCPServer()
	{
		closesocket(socketClient);
		closesocket(socketServer);
		WSACleanup();
	}

	void Accept()
	{
		int nSize = sizeof(clientAddr);
		socketClient = accept(socketServer, (SOCKADDR*)&clientAddr, &nSize);
		if (socketClient == INVALID_SOCKET) clear_and_exit();
	}

	auto Send(std::string toSend)
	{
		return send(socketClient, toSend.c_str(), strlen(toSend.c_str()) + 1, 0);
	}

	auto Recv()
	{
		char buf[MAXBYTE];
		recv(socketClient, buf, MAXBYTE, 0);
		return std::string(buf);
	}
	auto Recv(std::string& toRecv, bool append = false)
	{
		char buf[MAXBYTE];
		auto ret = recv(socketClient, buf, MAXBYTE, 0);
		if (!append) toRecv = std::string(buf);
		else toRecv.append(buf);
		return ret;
	}

	auto getIp(bool client = true)
	{
		char ipBuf[20];
		return std::string(inet_ntop(AF_INET, &(client ? clientAddr : serverAddr).sin_addr, ipBuf, 20));
	}
	auto getPort(bool client = true)
	{
		return ntohs((client ? clientAddr : serverAddr).sin_port);
	}
	auto getIpPort(bool client = true)
	{
		return getIp(client) + " - " + std::to_string(getPort(client));
	}

private:
	void clear_and_exit()
	{
		auto code = WSAGetLastError();
		closesocket(socketServer);
		WSACleanup();
		exit(auto code = WSAGetLastError(););
	}

private:
	WSADATA wsaData;
	SOCKET socketServer;
	sockaddr_in serverAddr;
	SOCKET socketClient;
	sockaddr_in clientAddr;
};

int main()
{
	TCPServer server;
	cout << "local ip - port: " << server.getIpPort(false) << endl;

	server.Accept();
	cout << "客户端成功接入 ip - port: " << server.getIpPort() << endl;

	cout << "Send: " << "Hello Client" << endl;
	server.Send("Hello Client");
	cout << "Recv: " << server.Recv() << endl;

	return 0;
}


/*
 * TCP 客户端程序
*/
#include <iostream>

using std::cout;
using std::endl;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")
#include <string>

/* WSAStartup()->socket()->connet()->send()/recv()->closesocket()->WSACleanup() */

class TCPClient
{
public:
	TCPClient()
	{
		if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) exit(EXIT_FAILURE);

		socketServer = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
		if (socketServer == INVALID_SOCKET)
		{
			auto code = WSAGetLastError();
			WSACleanup();
			exit(code);
		}

		serverAddr.sin_family = AF_INET;
		serverAddr.sin_port = htons(8082);
		inet_pton(AF_INET, "192.168.43.5", &serverAddr.sin_addr);
	}

	~TCPClient()
	{
		closesocket(socketServer);
		WSACleanup();
	}

	void Connect()
	{
		if (connect(socketServer, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) != 0)
		{
			auto code = WSAGetLastError();
			closesocket(socketServer);
			WSACleanup();
			exit(code);
		}
	}

	auto Send(std::string toSend)
	{
		return send(socketServer, toSend.c_str(), strlen(toSend.c_str()) + 1, 0);
	}

	auto Recv()
	{
		char buf[MAXBYTE];
		recv(socketServer, buf, MAXBYTE, 0);
		return std::string(buf);
	}
	auto Recv(std::string& toRecv, bool append = false)
	{
		char buf[MAXBYTE];
		auto ret = recv(socketServer, buf, MAXBYTE, 0);
		if (!append) toRecv = std::string(buf);
		else toRecv.append(buf);
		return ret;
	}

	auto getIp()
	{
		char ipBuf[20];
		return std::string(inet_ntop(AF_INET, &serverAddr.sin_addr, ipBuf, 20));
	}
	auto getPort()
	{
		return ntohs(serverAddr.sin_port);
	}
	auto getIpPort()
	{
		return getIp() + " - " + std::to_string(getPort());
	}

private:
	WSADATA wsaData;
	SOCKET socketServer;
	sockaddr_in serverAddr;
};

int main()
{
	TCPClient client;

	client.Connect();
    
	cout << "Recv: " << client.Recv() << endl;
	cout << "Send: " << "Hello Server" << endl;
	client.Send("Hello Server");

	return 0;
}
/*
 * TCP 服务端程序输出
*/
local ip - port: 192.168.43.5 - 8082
客户端成功接入 ip - port: 192.168.43.5 - 51249
Send: Hello Client
Recv: Hello Server

/*
 * TCP 客户端程序输出
*/
Recv: Hello Client
Send: Hello Server

4、UDP 封装

/*
 * UDP 服务端程序
*/
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>

using std::cout;
using std::endl;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")
#include <string>

/* WSAStartup()->socket()->bind()->sendto()/recvfrom()->closesocket()->WSACleanup() */

class UDPServer
{
public:
	UDPServer()
	{
		if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) exit(EXIT_FAILURE);

		mySocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
		if (mySocket == INVALID_SOCKET)
		{
			auto code = WSAGetLastError();
			WSACleanup();
			exit(code);
		}

		char hostname[MAXBYTE];
		gethostname(hostname, MAXBYTE);
		serverAddr.sin_family = AF_INET;
		serverAddr.sin_port = htons(8082);
		serverAddr.sin_addr.S_un.S_addr = ((struct in_addr*)gethostbyname(hostname)->h_addr)->s_addr;

		if (bind(mySocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) != 0)
		{
			auto code = WSAGetLastError();
			closesocket(mySocket);
			WSACleanup();
			exit(code);
		}
	}

	~UDPServer()
	{
		closesocket(mySocket);
		WSACleanup();
	}

	auto Sendto(std::string toSend)
	{
		return sendto(mySocket, toSend.c_str(), strlen(toSend.c_str()) + 1, 0, (SOCKADDR*)&clientAddr, sizeof(clientAddr));
	}

	auto Recvfrom()
	{
		char buf[MAXBYTE];
		recvfrom(mySocket, buf, 256, 0, (SOCKADDR*)&clientAddr, &nSize);
		return std::string(buf);
	}
	auto Recv(std::string& toRecv, bool append = false)
	{
		char buf[MAXBYTE];
		auto ret = recvfrom(mySocket, buf, 256, 0, (SOCKADDR*)&clientAddr, &nSize);
		if (!append) toRecv = std::string(buf);
		else toRecv.append(buf);
		return ret;
	}

	auto getIp(bool client = true)
	{
		char ipBuf[20];
		return std::string(inet_ntop(AF_INET, &(client ? clientAddr : serverAddr).sin_addr, ipBuf, 20));
	}
	auto getPort(bool client = true)
	{
		return ntohs((client ? clientAddr : serverAddr).sin_port);
	}
	auto getIpPort(bool client = true)
	{
		return getIp(client) + " - " + std::to_string(getPort(client));
	}

private:
	WSADATA wsaData;
	SOCKET mySocket;
	sockaddr_in serverAddr;
	sockaddr_in clientAddr;
	int nSize = sizeof(clientAddr);
};

int main()
{
	UDPServer server;
	cout << "local ip - port: " << server.getIpPort(false) << endl;

	cout << "Recv: " << server.Recvfrom() << endl;
	cout << "客户端成功接入 ip - port: " << server.getIpPort() << endl;
	cout << "Send: " << "Hello Client" << endl;
	server.Sendto("Hello Client");

	return 0;
}


/*
 * UDP 客户端程序
*/
#include <iostream>

using std::cout;
using std::endl;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")
#include <string>

/* WSAStartup()->socket()->sendto()/recvfrom()->closesocket()->WSACleanup() */

class UDPClient
{
public:
	UDPClient()
	{
		if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) exit(EXIT_FAILURE);

		mySocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
		if (mySocket == INVALID_SOCKET)
		{
			auto code = WSAGetLastError();
			WSACleanup();
			exit(code);
		}

		serverAddr.sin_family = AF_INET;
		serverAddr.sin_port = htons(8082);
		inet_pton(AF_INET, "192.168.43.5", &serverAddr.sin_addr);
	}

	~UDPClient()
	{
		closesocket(mySocket);
		WSACleanup();
	}

	auto Sendto(std::string toSend)
	{
		return sendto(mySocket, toSend.c_str(), strlen(toSend.c_str()) + 1, 0, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
	}

	auto Recvfrom()
	{
		char buf[MAXBYTE];
		recvfrom(mySocket, buf, MAXBYTE, 0, (SOCKADDR*)&serverAddr, &nSize);
		return std::string(buf);
	}
	auto Recvfrom(std::string& toRecv, bool append = false)
	{
		char buf[MAXBYTE];
		auto ret = recvfrom(mySocket, buf, MAXBYTE, 0, (SOCKADDR*)&serverAddr, &nSize);
		if (!append) toRecv = std::string(buf);
		else toRecv.append(buf);
		return ret;
	}

	auto getIp()
	{
		char ipBuf[20];
		return std::string(inet_ntop(AF_INET, &serverAddr.sin_addr, ipBuf, 20));
	}
	auto getPort()
	{
		return ntohs(serverAddr.sin_port);
	}
	auto getIpPort()
	{
		char ipBuf[20];
		return getIp() + " - " + std::to_string(getPort());
	}

private:
	WSADATA wsaData;
	SOCKET mySocket;
	sockaddr_in serverAddr;
	int nSize = sizeof(serverAddr);
};

int main()
{
	UDPClient client;

	cout << "Send: " << "Hello Server" << endl;
	client.Sendto("Hello Server");
	cout << "Recv: " << client.Recvfrom() << endl;

	return 0;
}
/*
 * UDP 服务端程序输出
*/
local ip - port: 192.168.43.5 - 8082
Recv: Hello Server
客户端成功接入 ip - port: 192.168.43.5 - 54075
Send: Hello Client

/*
 * UDP 客户端程序输出
*/
Send: Hello Server
Recv: Hello Client

七、借助多线程实现 TCP 全双工通信

// 为避免标准输出的竞争,发送消息通过检测按键,将相应值拷贝四次后进行发送
// 如:按下 1,发送为 11111

/*
 * TCP 服务端程序
 * TCPServer 类见 [六-3] TCP 封装
*/
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>

using std::cout;
using std::endl;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")
#include <string>
#include <thread>
#include <atomic>
#include <conio.h>

/* WSAStartup()->socket()->bind()->listen()->accept()->send()/recv()->closesocket()->WSACleanup() */

// 同 [六-3] TCP 封装
class TCPServer;

int main()
{
	TCPServer server;
	cout << "local ip - port: " << server.getIpPort(false) << endl << endl;
	cout << "等待客户端接入..." << endl;

	std::atomic_bool disconnect = false;
	std::atomic_flag ostream_spin_lock = ATOMIC_FLAG_INIT;

	while (true)
	{
		server.Accept();
		cout << "客户端成功接入, ip - port: " << server.getIpPort() << endl;
		disconnect = false;

		std::thread thread_recv(
			[&] {
				while (true)
				{
					std::string recvMsg;
					auto recvRet = server.Recv(recvMsg);
					if (recvRet == SOCKET_ERROR)
					{
						disconnect.store(true);
						return;
					}
					while (!ostream_spin_lock.test_and_set()) std::this_thread::sleep_for(std::chrono::milliseconds(10));
					cout << "Recv:\n\t" << recvMsg << endl;
					ostream_spin_lock.clear();
				}
			}
		);
		thread_recv.detach();

		while (!disconnect.load() && !(GetAsyncKeyState(VK_ESCAPE) & 0x8000))
		{
			if (_kbhit())
			{
				std::string sendMsg(5, static_cast<char>(_getch()));
				server.Send(sendMsg);
				while (!ostream_spin_lock.test_and_set()) std::this_thread::sleep_for(std::chrono::milliseconds(10));
				cout << "Send:\n\t" << sendMsg << endl;
				ostream_spin_lock.clear();
			}
			std::this_thread::sleep_for(std::chrono::milliseconds(10));
		}

		cout << "客户端断开连接,等待重新接入..." << endl << endl;
	}

	return 0;
}


/*
 * TCP 客户端程序
 * TCPClient 类见 [六-3] TCP 封装
*/
#include <iostream>

using std::cout;
using std::endl;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")
#include <string>
#include <thread>
#include <atomic>
#include <conio.h>

/* WSAStartup()->socket()->connet()->send()/recv()->closesocket()->WSACleanup() */

// 同 [六-3] TCP 封装
class TCPClient;

int main()
{
	TCPClient client;

	std::atomic_bool disconnect = false;
	std::atomic_flag ostream_spin_lock = ATOMIC_FLAG_INIT;

	client.Connect();
	cout << "成功接入服务端,ip - port: " << client.getIpPort() << endl;
	disconnect = false;

	std::thread thread_recv(
		[&] {
			while (true)
			{
				std::string recvMsg;
				auto recvRet = client.Recv(recvMsg);
				if (recvRet == SOCKET_ERROR)
				{
					disconnect.store(true);
					return;
				}
				while (!ostream_spin_lock.test_and_set()) std::this_thread::sleep_for(std::chrono::milliseconds(10));
				cout << "Recv:\n\t" << recvMsg << endl;
				ostream_spin_lock.clear();
			}
		}
	);
	thread_recv.detach();

	while (!disconnect.load() && !(GetAsyncKeyState(VK_ESCAPE) & 0x8000))
	{
		if (_kbhit())
		{
			std::string sendMsg(5, static_cast<char>(_getch()));
			client.Send(sendMsg);
			while (!ostream_spin_lock.test_and_set()) std::this_thread::sleep_for(std::chrono::milliseconds(10));
			cout << "Send:\n\t" << sendMsg << endl;
			ostream_spin_lock.clear();
		}
		std::this_thread::sleep_for(std::chrono::milliseconds(10));
	}

	cout << "服务端断开连接..." << endl;

	return 0;
}
// 客户端发送 1,服务端发送 2,客户端发送 3
// 此后客户端断开重连后重复上面操作

/*
 * TCP 服务端程序输出
*/
local ip - port: 192.168.43.5 - 8082

等待客户端接入...
客户端成功接入, ip - port: 192.168.43.5 - 53231
Recv:
	11111
Send:
	22222
Recv:
	33333
客户端断开连接,等待重新接入...

客户端成功接入, ip - port: 192.168.43.5 - 53233
Recv:
	11111
Send:
	22222
Recv:
	33333

/*
 * TCP 客户端程序输出
*/
成功接入服务端,ip - port: 192.168.43.5 - 8082
Send:
	11111
Recv:
	22222
Send:
	33333
// 断开重连
成功接入服务端,ip - port: 192.168.43.5 - 8082
Send:
	11111
Recv:
	22222
Send:
	33333

八、TCP 多连接多请求设计

  1. 试想,有许多客户端向服务端发起请求,服务端需要自动返回相关内容怎么办?

  2. 思考几个问题:

    • 如何处理众多客户端的连接请求,也就是如何使用阻塞模式的 accept
    • 如何接收众多客户端的消息,并自动返回返回内容,注意 recv 是阻塞的
    • 用什么数据结构存储众多客户端的 SOCKET 以及 sockaddr_in
    • 某个客户端断开连接后通过什么途径释放资源,也就是如何使用 closesocket
  3. 先假设一个通信场景:

    • 为避免竞争 IO 流,直接处理客户端按键消息,如:按下 1 则发送 11111
    • 服务器将信息接收消息追加返回,如收到 “11111”,返回 “1111111111”
  4. 一个解决方案:

    • 用一个线程 thread1 循环执行 accept
    • thread1 线程执行 accept 成功后打印新连接信息,并生成一个线程 threadx 循环执行 recv,threadx 根据 recv 内容进行返回内容,同时 thread1 将线程 threadx 的 id 与得到的 SOCKET 和 sockaddr_in 进行绑定放入哈希表,key 为 thread_id,value 为 pair 对象,存储 SOCKET 和 sockaddr_in
    • 线程 threadx 执行 recv 及 send 时的参数可通过自身 thread_id 检索哈希表得到,当 recv 返回 SOCKET_ERROR 认定对方断开,打印连接结束信息,并调用 closesocket,然后删除哈希表中的记录,最后结束线程自身
  5. 上述方案存在的问题:

    • 输出流竞争:这里采用自旋锁解决

    • 多线程共享哈希表存在竞争,如线信息存入哈希表时,可能存在其他线程从哈希表删除内容,若 thread_id 哈希值相同,则存在同时修改同一个位置的情况,因此需要设计一个安全的哈希表

    • 一个简单的并发哈希表设计:这里为了简单,将插入哈希表操作、从哈希表删除操作和读操作进行串行化,即三个操作共用一把互斥锁(也可用自旋锁,两种锁的区别可自行学习)。带来的问题是对并发的不友好:当 thread_id 的哈希值不同时,多个线程插入、删除和读操作不会发生竞争(除开扩容操作),但任然被串行化

  6. 代码演示:

    /*
     * TCP 服务端程序
    */
    #define _WINSOCK_DEPRECATED_NO_WARNINGS
    #include <iostream>
    
    using std::cout;
    using std::endl;
    
    #include <WinSock2.h>
    #include <WS2tcpip.h>
    #pragma comment (lib, "ws2_32")
    #include <string>
    #include <thread>
    #include <mutex>
    #include <atomic>
    #include <conio.h>
    #include <unordered_map>
    
    /* WSAStartup()->socket()->bind()->listen()->accept()->send()/recv()->closesocket()->WSACleanup() */
    
    class TCPServer
    {
    public:
    	TCPServer()
    	{
    		if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) exit(EXIT_FAILURE);
    
    		socketServer = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    		if (socketServer == INVALID_SOCKET)
    		{
    			auto code = WSAGetLastError();
    			WSACleanup();
    			exit(code);
    		}
    
    		char hostname[MAXBYTE];
    		gethostname(hostname, MAXBYTE);
    		serverAddr.sin_family = AF_INET;
    		serverAddr.sin_port = htons(8082);
    		serverAddr.sin_addr.S_un.S_addr = ((struct in_addr*)gethostbyname(hostname)->h_addr)->s_addr;
    
    		if (bind(socketServer, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) != 0) clear_and_exit();
    		if (listen(socketServer, SOMAXCONN) != 0) clear_and_exit();
    	}
    
    	~TCPServer()
    	{
    		closesocket(socketServer);
    		WSACleanup();
    	}
    
    	void Accept(SOCKET& socketClient, sockaddr_in& clientAddr)
    	{
    		int nSize = sizeof(clientAddr);
    		socketClient = accept(socketServer, (SOCKADDR*)&clientAddr, &nSize);
    		if (socketClient == INVALID_SOCKET) clear_and_exit();
    	}
    
    	auto Send(std::string toSend, SOCKET socketClient)
    	{
    		return send(socketClient, toSend.c_str(), strlen(toSend.c_str()) + 1, 0);
    	}
    
    	auto Recv(SOCKET socketClient)
    	{
    		char buf[MAXBYTE];
    		recv(socketClient, buf, MAXBYTE, 0);
    		return std::string(buf);
    	}
    	auto Recv(std::string& toRecv, SOCKET socketClient, bool append = false)
    	{
    		char buf[MAXBYTE];
    		auto ret = recv(socketClient, buf, MAXBYTE, 0);
    		if (!append) toRecv = std::string(buf);
    		else toRecv.append(buf);
    		return ret;
    	}
    
    	auto getIp(const sockaddr_in& clientAddr)
    	{
    		char ipBuf[20];
    		return std::string(inet_ntop(AF_INET, &clientAddr.sin_addr, ipBuf, 20));
    	}
    	auto getPort(const sockaddr_in& clientAddr)
    	{
    		return ntohs(clientAddr.sin_port);
    	}
    	auto getIpPort(const sockaddr_in& clientAddr)
    	{
    		return getIp(clientAddr) + " - " + std::to_string(getPort(clientAddr));
    	}
    
    	void insertToMap(std::thread::id id, SOCKET s, sockaddr_in addr)
    	{
    		std::lock_guard<std::mutex> auto_lock(mtx);
    		mp.insert({ id, std::make_pair(s,addr) });
    	}
    	void eraseFromMap(std::thread::id id)
    	{
    		std::lock_guard<std::mutex> auto_lock(mtx);
    		mp.erase(id);
    	}
    	auto getFromMap(std::thread::id id)
    	{
    		std::lock_guard<std::mutex> auto_lock(mtx);
    		return mp[id];
    	}
    
    private:
    	void clear_and_exit()
    	{
    		auto code = WSAGetLastError();
    		closesocket(socketServer);
    		WSACleanup();
    		exit(code);
    	}
    
    private:
    	WSADATA wsaData;
    	SOCKET socketServer;
    	sockaddr_in serverAddr;
    	std::unordered_map<std::thread::id, std::pair<SOCKET, sockaddr_in>> mp;
    	std::mutex mtx;
    };
    
    int main()
    {
    	TCPServer server;
    
    	std::atomic_flag ostream_spin_lock = ATOMIC_FLAG_INIT;
    
    	auto recv_send = [&]
    	{
    		while (true)
    		{
    			std::string recvMsg;
    			auto clientMsg = server.getFromMap(std::this_thread::get_id());
    			auto recvRet = server.Recv(recvMsg, clientMsg.first);
    			if (recvRet == SOCKET_ERROR)
    			{
    				while (!ostream_spin_lock.test_and_set()) std::this_thread::sleep_for(std::chrono::milliseconds(10));
    				cout << "连接结束: " << server.getIpPort(clientMsg.second) << endl;
    				ostream_spin_lock.clear();
    				closesocket(clientMsg.first);
    				server.eraseFromMap(std::this_thread::get_id());
    				return;
    			}
    			else server.Send(recvMsg + recvMsg, clientMsg.first);
    		}
    	};
    
    	while (true)
    	{
    		SOCKET socketClient;
    		sockaddr_in clientAddr;
    		server.Accept(socketClient, clientAddr);
    
    		while (!ostream_spin_lock.test_and_set()) std::this_thread::sleep_for(std::chrono::milliseconds(10));
    		cout << "新连接接入: " << server.getIpPort(clientAddr) << endl;
    		ostream_spin_lock.clear();
    
    		std::thread newThread(recv_send);
    		server.insertToMap(newThread.get_id(), socketClient, clientAddr);
    
    		newThread.detach();
    	}
    
    	return 0;
    }
    
    /*
     * TCP 客户端端程序见 [六-3] TCP 封装
    */
    
  7. 输出演示:运行服务端,然后依次开启三个客户端,然后三个客户端依次按下 1,2,3,最后逆序关闭三个客户端

    /*
     * 客户端 1 输出
    */
    成功接入服务端,ip - port: 192.168.43.5 - 8082
    Send:
    	11111
    Recv:
    	1111111111
    /*
     * 客户端 2 输出
    */
    成功接入服务端,ip - port: 192.168.43.5 - 8082
    Send:
    	22222
    Recv:
    	2222222222
    /*
     * 客户端 3 输出
    */
    成功接入服务端,ip - port: 192.168.43.5 - 8082
    Send:
    	33333
    Recv:
    	3333333333
    /*
     * 服务端输出
    */
    新连接接入: 192.168.43.5 - 56168
    新连接接入: 192.168.43.5 - 56170
    新连接接入: 192.168.43.5 - 56172
    连接结束: 192.168.43.5 - 56172
    连接结束: 192.168.43.5 - 56170
    连接结束: 192.168.43.5 - 56168
    

九、Winsock 其他函数

1、getsockname

  • 作用:getsockname() 函数用于获取一个套接字的名字。它用于一个已捆绑或已连接套接字 s,本地地址将被返回。本调用特别适用于如下情况:未调用 bind() 就调用了 connect(),这时唯有 getsockname() 调用可以获知系统内定的本地地址。在返回时,namelen 参数包含了名字的实际字节数

  • 函数原型:

    int getsockname(SOCKET s, struct sockaddr* name, int* namelen);
    
  • 参数:

    • s:标识一个已捆绑套接口的描述字
    • name:接收套接口的地址
    • namelen:名字缓冲区长度
  • 返回值:

    • 0 成功
    • 否则返回 SOCKET_ERROR 错误,应用程序可通过 WSAGetLastError() 获取相应错误代码
  • 使用:返回本地 ip-port,如在 TCP 客户端 connect 后调用

    auto getLocalMsg()
    {
    	sockaddr_in localAddr;
    	int len = sizeof(localAddr);
    	if (getsockname(socketServer, (SOCKADDR*)&localAddr, &len) == 0)
    	{
    		char buf[20];
    		return std::string(inet_ntop(AF_INET, &localAddr.sin_addr, buf, 20)) + " - " + std::to_string(ntohs(localAddr.sin_port));
    	}
    	else return std::string();
    }
    

2、shutdown

  • 作用:禁止在一个套接口上进行数据的接收与发送

  • 函数原型:

    int shutdown(SOCKET s, int how);
    
  • 参数:

    • s:用于标识一个套接口的描述字
    • how:标志,用于描述禁止哪些操作。取值及其意义如下:
      • SD_RECEIVE(0):关闭 s 上的读操作,后续接收操作将被禁止。这对于低层协议无影响。对于 TCP 协议,TCP 窗口不改变并接收前来的数据(但不确认)直至窗口满。对于 UDP 协议,接收并排队前来的数据。任何情况下都不会产生 ICMP 错误包
      • SD_SEND(1):关闭 s 上的写操作,禁止后续发送操作。对于TCP,将发送FIN
      • SD_BOTH(2):关闭 s 上的读写操作,同时禁止收和发
  • 返回值:

    • 0 成功
    • 否则返回 SOCKET_ERROR 错误,应用程序可通过 WSAGetLastError() 获取相应错误代码

3、setsockopt

  • 作用:用于任意类型、任意状态套接口的设置选项值。尽管在不同协议层上存在选项,但本函数仅定义了最高的 “套接口” 层次上的选项

  • 函数原型:

    int setsockopt(
    	SOCKET s,
    	int level,
    	int optname,
    	const char* optval,
    	int optlen
    );
    
  • 参数:

    • s:用于标识一个套接口的描述字
    • level:选项定义的层次。目前仅支持 SOL_SOCKET 和 IPPROTO_TCP 层次。分别表示 Socket 属性和 TCP 属性
    • optname:需设置的选项
    • optval:指向存放选项值的缓冲区
    • optlen:指向 optval 缓冲区的长度值
  • 返回值:

    • 0 成功
    • 否则返回 SOCKET_ERROR 错误,应用程序可通过 WSAGetLastError() 获取相应错误代码
  • 更详细的内容请参考:百度词条 setsockopt

  • 这里说一个应用场景:单片机与 TCP 后端通信,当单片机断电后,后端并不能收到任何消息,若单片机会定时发送数据给后端,则可通过 setsockopt 设置接收超时返回,而不是一直阻塞在 recv() 处,如下:

    int nNetTimeout = 1000;		// 1 秒
    setsockopt(socketServer, SOL_SOCKET, SO_RCVTIMEO, (char*)&nNetTimeout, sizeof(int));
    

4、getsockopt

  • 作用:用于获取任意类型、任意状态套接口的选项当前值,并把结果存入 optval

  • 函数原型:

    int getsockopt(
    	SOCKET s,
    	int level,
    	int optname,
    	char* optval,
    	int* optlen
    );
    
  • 参数:

    • s:用于标识一个套接口的描述字
    • level:选项定义的层次。目前仅支持 SOL_SOCKET 和 IPPROTO_TCP 层次。分别表示 Socket 属性和 TCP 属性
    • optname:需获取的套接口选项
    • optval:指向存放所获得选项值的缓冲区
    • optlen:指向 optval 缓冲区的长度值
  • 返回值:

    • 0 成功
    • 否则返回 SOCKET_ERROR 错误,应用程序可通过 WSAGetLastError() 获取相应错误代码
  • 更详细的内容请参考:百度词条 getsockopt

5、ioctlsocket

  • 作用:控制套接口的模式。可用于任一状态的任一套接口。它用于获取与套接口相关的操作参数,而与具体协议或通讯子系统无关

  • 函数原型:

    int ioctlsocket(SOCKET s, long cmd, u_long* argp);
    
  • 参数:

    • s:一个标识套接口的描述字
    • cmd:对套接口 s 的操作命令
    • argp:指向 cmd 命令所带参数的指针
  • 返回值:

    • 0 成功
    • 否则返回 SOCKET_ERROR 错误,应用程序可通过 WSAGetLastError() 获取相应错误代码
  • 更详细的内容请参考:百度词条 ioctlsocket

6、WSAAsyncSelect

  • 作用:用来请求 Windows Sockets DLL 为窗口句柄发一条消息:无论它何时检测到由 lEvent 参数指明的网络事件,要发送的消息由 wMsg 参数标明,被通知的套接口由 s 标识

  • 函数原型:

    int WSAAsyncSelect(
    	SOCKET s,
    	HWND hWnd,
    	u_int wMsg,
    	long lEvent
    );
    
  • 参数:

    • s:标识一个需要事件通知的套接口的描述符
    • hWnd:标识一个在网络事件发生时需要接收消息的窗口句柄
    • wMsg:在网络事件发生时要接收的消息
    • lEvent:位屏蔽码,用于指明应用程序感兴趣的网络事件集合
  • 返回值:

    • 0 成功
    • 否则返回 SOCKET_ERROR 错误,应用程序可通过 WSAGetLastError() 获取相应错误代码
  • 更详细的内容请参考:百度词条 WSAAsyncSelect

版权声明:本文为teternity原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/teternity/p/WinSock.html