详解服务端/客户端|TCP网络通信编程|C++|MFC(上)

文章内容

  • 0 前言
  • 1 客户端程序测试(connect)
    • 1.1 connect代码
    • 1.2 运行结果
    • 1.3 客户端简易模型
    • 1.4 客户端测试(默认端口号记录)
    • 1.5 【无】127.0.0.1和27015
    • 1.6 不存在从string到从const char*的适当转换函数
  • 2 服务端(bind)
  • 2.1 bind()服务端代码
  • 2.2 运行结果
    • 2.3 服务端简易模型
  • 3 服务端客户端通信:accept()、recv()
    • 3.1 recv()和accpet()
      • 3.1.1 recv()
      • 3.1.2 accept()
    • 3.2 运行结果
    • 3.3 服务端/客户端模型
    • 3.4 const char*类型的值不能用于初始化char*类型的实体
  • 4 一对一收发(非线程)
    • 4.1 一对一通信
    • 4.2 代码如下:
      • 4.2.1 客户端代码
      • 4.2.2 服务端代码
      • 4.2.3运行结果如下:
    • 4.3 【未】字符串终结符’\0’,TCP发送无边界
  • 5 一对多收发(线程)
    • 5.1 线程运行
    • 5.2 一对多
      • 5.2.1 程序如下:
        • 5.2.1.1 客户端
        • 5.2.1.2 服务端
        • 5.1.2.3 运行结果
  • 参考文献【未贴完,今天寝室要关门了】

0 前言

记录一下简易的客户端-服务端的Socket通信。
MFC版放在下一篇。

文末附参考文献及引申阅读。
文中代码直接复制即可运行。
也可后台回复「20210819」,获取源代码工程文件,点击**.sln**即可运行。
详解服务端/客户端|TCP网络通信编程|C++|MFC(上)_第1张图片

1 客户端程序测试(connect)

先从MSDN官方例子开始,抠出通信模型。
在搜索引擎上输入“msdn connect”,查看connect()函数的说明。
链接:connect function (winsock2.h) - Win32 apps | Microsoft Docs
详解服务端/客户端|TCP网络通信编程|C++|MFC(上)_第2张图片
复制“Example Code”代码,在VS新建程序,如下。详解服务端/客户端|TCP网络通信编程|C++|MFC(上)_第3张图片

1.1 connect代码

//代码来自:https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-connect
//注:修改部分后,最终代码如下

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#ifndef UNICODE
#define UNICODE
#endif

#define WIN32_LEAN_AND_MEAN

#include 
#include 
#include 
#include 	//std::string
//#include 

// Need to link with Ws2_32.lib
#pragma comment(lib, "ws2_32.lib")

int main(int argc, char* argv[])
{
	
	// Initialize Winsock
	//==【1】==初始化Winsock。使用WSAStartup()函数,这是Windows系统特有的要求,Linux系统不需要;
	WSADATA wsaData;
	int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != NO_ERROR) {
		printf("WSAStartup function failed with error: %d\n", iResult);
		return 1;
	}

	// Create a SOCKET for connecting to server
	//==【2】==创建一个SOCKET类型的TCP通信字。使用socket()函数;
	SOCKET ConnectSocket;
	ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (ConnectSocket == INVALID_SOCKET) {
		printf("socket function failed with error: %ld\n", WSAGetLastError());
		WSACleanup();
		return 1;
	}

	// The sockaddr_in structure specifies the address family,
	// IP address, and port of the server to be connected to.
	//==【3】==向sockaddr_in结构体中填写服务器IP地址、端口号以及通信协议类型;
	sockaddr_in clientService;
	std::string serverIP = "127.0.0.1";	
	unsigned short serverPORT = 27015;
	
	clientService.sin_family = AF_INET;
	clientService.sin_addr.s_addr = inet_addr(serverIP.c_str());//clientService.sin_addr.s_addr = inet_addr("127.0.0.1");//std::string到const char*转换转换【serverIP→serverIP.c_str()】
	clientService.sin_port = htons(serverPORT);					//clientService.sin_port = htons(27015);

	// Connect to server.
	//==【4】==用connect()函数向服务器发起TCP连接请求,connect()函数返回值0表达连接成功,否则连接失败,程序终止;
	iResult = connect(ConnectSocket, (SOCKADDR *)& clientService, sizeof(clientService));
	if (iResult == SOCKET_ERROR) {
		printf("connect function failed with error: %ld\n", WSAGetLastError());
		iResult = closesocket(ConnectSocket);
		if (iResult == SOCKET_ERROR)
			printf("closesocket function failed with error: %ld\n", WSAGetLastError());
		WSACleanup();
		return 1;
	}

	printf("Connected to server.\n");

	iResult = closesocket(ConnectSocket);
	if (iResult == SOCKET_ERROR) {
		printf("closesocket function failed with error: %ld\n", WSAGetLastError());
		WSACleanup();
		return 1;
	}

	WSACleanup();
	return 0;
}

1.2 运行结果

详解服务端/客户端|TCP网络通信编程|C++|MFC(上)_第4张图片
将端口号“27015”改成“666”,运行结果如下:连接失败。
详解服务端/客户端|TCP网络通信编程|C++|MFC(上)_第5张图片

1.3 客户端简易模型

抠出客户端简易模型(不含收发信息):
创建Socket→填写sockaddr_in结构体→connect()连接
详解服务端/客户端|TCP网络通信编程|C++|MFC(上)_第6张图片

1.4 客户端测试(默认端口号记录)

①HTTP服务器,默认的端口号为80/tcp(木马Executor开放此端口);
②HTTPS(securely transferring web pages)服务器,默认的端口号为443/tcp 443/udp;

在控制台中输入“ping www.baidu.com”,获得网站的IP地址,并替换“127.0.0.1”。
详解服务端/客户端|TCP网络通信编程|C++|MFC(上)_第7张图片
IP地址:182.61.200.7
端口号:443
运行结果,连接成功。
详解服务端/客户端|TCP网络通信编程|C++|MFC(上)_第8张图片

1.5 【无】127.0.0.1和27015

1.6 不存在从string到从const char*的适当转换函数

修改:clientService.sin_addr.s_addr = inet_addr(serverIP.c_str());
详解服务端/客户端|TCP网络通信编程|C++|MFC(上)_第9张图片

2 服务端(bind)

接第1节,输入“bind”,搜索查看bind()函数相关信息。
链接:bind function (winsock.h) - Win32 apps | Microsoft Docs
详解服务端/客户端|TCP网络通信编程|C++|MFC(上)_第10张图片

2.1 bind()服务端代码

修改后,最终代码如下:

//以下大部分代码来自:https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-bind
#define _WINSOCK_DEPRECATED_NO_WARNINGS

#ifndef UNICODE
#define UNICODE
#endif

#define WIN32_LEAN_AND_MEAN

#include 
#include 
#include 
#include 	//std::string

// Link with ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

int main(int argc, char* argv[])
{

	// Initialize Winsock
	//==【1】==初始化Winsock。函数WSAStartup()
	int iResult = 0;            // used to return function results
	WSADATA wsaData;
	iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != NO_ERROR) {
		printf("Error at WSAStartup()\n");
		return 1;
	}

	// Create a SOCKET for listening for incoming connection requests
	//==【2】==创建Socket。使用socket()函数。
	SOCKET ListenSocket = INVALID_SOCKET;
	ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (ListenSocket == INVALID_SOCKET) {
		printf("socket function failed with error: %u\n", WSAGetLastError());
		WSACleanup();
		return 1;
	}
	
	// The sockaddr_in structure specifies the address family,IP address, and port for the socket that is being bound.
	//==【3】==向sockaddr_in结构体填写信息。网卡IP地址、服务端口号以及通信协议类型。
	sockaddr_in service;	// The socket address to be passed to bind
	std::string serverIP = "127.0.0.1";
	unsigned short serverPORT = 666;

	service.sin_family = AF_INET;
	service.sin_addr.s_addr = inet_addr(serverIP.c_str());
	service.sin_port = htons(serverPORT);

	// Bind the socket.
	//==【4】==绑定socket。函数bind()。绑定后,其他程序不能再使用这个端口号;如果其他程序已经绑定了,则这里绑定失败,程序终止
	iResult = bind(ListenSocket, (SOCKADDR *)&service, sizeof(service));
	if (iResult == SOCKET_ERROR) {
		printf("bind failed with error %u\n", WSAGetLastError());
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}
	else
		printf("bind returned success\n");

	WSACleanup();
	return 0;
}

2.2 运行结果

详解服务端/客户端|TCP网络通信编程|C++|MFC(上)_第11张图片

2.3 服务端简易模型

服务端简易模型如下:
详解服务端/客户端|TCP网络通信编程|C++|MFC(上)_第12张图片

3 服务端客户端通信:accept()、recv()

3.1 recv()和accpet()

accept function (winsock2.h) - Win32 apps | Microsoft Docs
recv function (winsock.h) - Win32 apps | Microsoft Docs
以上两个函数,组成一对。

3.1.1 recv()

最终代码如下:

//【recv】https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-recv
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define WIN32_LEAN_AND_MEAN

#include 
#include 
#include 
#include 

// Link with ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"

int main(int argc, char* argv[]) {

	
	// Declare and initialize variables.
	const char *sendbuf = "this is a test";
	char recvbuf[DEFAULT_BUFLEN];
	int recvbuflen = DEFAULT_BUFLEN;

	// Initialize Winsock
	//==【1】==初始化Winsock,WSAStart()
	WSADATA wsaData;
	int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != NO_ERROR) {
		printf("WSAStartup failed: %d\n", iResult);
		return 1;
	}

	// Create a SOCKET for connecting to server
	//==【2】==socket()函数,创建一个socket
	SOCKET ConnectSocket = INVALID_SOCKET;
	ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (ConnectSocket == INVALID_SOCKET) {
		printf("Error at socket(): %ld\n", WSAGetLastError());
		WSACleanup();
		return 1;
	}

	// The sockaddr_in structure specifies the address family, IP address, and port of the server to be connected to.
	//==【3】==向sockaddr_in结构体填写信息
	struct sockaddr_in clientService;
	std::string serverIP = "127.0.0.1";
	unsigned short serverPORT = 666;

	clientService.sin_family = AF_INET;
	clientService.sin_addr.s_addr = inet_addr(serverIP.c_str());
	clientService.sin_port = htons(serverPORT);

	// Connect to server.
	//==【4】==connect()函数,连接server
	iResult = connect(ConnectSocket, (SOCKADDR*)&clientService, sizeof(clientService));
	if (iResult == SOCKET_ERROR) {
		closesocket(ConnectSocket);
		printf("Unable to connect to server: %ld\n", WSAGetLastError());
		WSACleanup();
		return 1;
	}

	// Send an initial buffer
	//==【5】==send()函数发送消息
	iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
	if (iResult == SOCKET_ERROR) {
		printf("send failed: %d\n", WSAGetLastError());
		closesocket(ConnectSocket);
		WSACleanup();
		return 1;
	}

	printf("Bytes Sent: %ld\n", iResult);

	// shutdown the connection since no more data will be sent
	iResult = shutdown(ConnectSocket, SD_SEND);
	if (iResult == SOCKET_ERROR) {
		printf("shutdown failed: %d\n", WSAGetLastError());
		closesocket(ConnectSocket);
		WSACleanup();
		return 1;
	}

	// Receive until the peer closes the connection
	//==【6】==recv()函数接收消息
	do {

		iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
		if (iResult > 0)
			printf("Bytes received: %d\n", iResult);
		else if (iResult == 0)
			printf("Connection closed\n");
		else
			printf("recv failed: %d\n", WSAGetLastError());

	} while (iResult > 0);

	// cleanup
	closesocket(ConnectSocket);
	WSACleanup();

	return 0;
}

3.1.2 accept()

//【accept】https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-accept

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#ifndef UNICODE
#define UNICODE
#endif

#include 
#include 
#include 
#include 

// Need to link with Ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

int main(int argc, char* argv[])
{

	// Initialize Winsock.
	//==【1】==初始化Winsock,WSAStartup()函数。
	WSADATA wsaData;
	int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != NO_ERROR) {
		printf("WSAStartup failed with error: %ld\n", iResult);
		return 1;
	}
	
	// Create a SOCKET for listening for incoming connection requests.
	//==【2】==创建socket,socket()函数。
	SOCKET ListenSocket;
	ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (ListenSocket == INVALID_SOCKET) {
		printf("socket failed with error: %ld\n", WSAGetLastError());
		WSACleanup();
		return 1;
	}

	// The sockaddr_in structure specifies the address family, IP address, and port for the socket that is being bound.
	//==【3】==向sockaddr_in结构体写信息。
	sockaddr_in service;
	std::string serverIP = "0.0.0.0";
	unsigned short serverPORT = 666;

	service.sin_family = AF_INET;
	service.sin_addr.s_addr = inet_addr(serverIP.c_str());
	service.sin_port = htons(serverPORT);
	
	//==【4】==绑定socket,bind()函数
	iResult = bind(ListenSocket, (SOCKADDR *)& service, sizeof(service));
	if (iResult == SOCKET_ERROR) {
		printf("bind failed with error: %ld\n", WSAGetLastError());
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}

	// Listen for incoming connection requests. on the created socket
	//==【5】==用listen()函数将ListenSocket置于监听状态,表达这个SOCKET仅仅用于接收客户端连接,而不是用于数据通信;
	iResult = listen(ListenSocket, 6); //6表达这个SOCKET最多允许7个客户端与之同时保持TCP连接;
	if (iResult == SOCKET_ERROR) {
		printf("listen failed with error: %ld\n", WSAGetLastError());
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}

	// Create a SOCKET for accepting incoming requests.	Accept the connection.
	//==【6】==创建连接关系的socket。用accept函数接收客户端连接。
	SOCKET AcceptSocket;
	printf("Waiting for client to connect...\n");
	AcceptSocket = accept(ListenSocket, NULL, NULL);
	if (AcceptSocket == INVALID_SOCKET) {
		printf("accept failed with error: %ld\n", WSAGetLastError());
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}
	else
		printf("Client connected.\n");

	// No longer need server socket
	closesocket(ListenSocket);

	WSACleanup();
	return 0;
}

3.2 运行结果

运行结果如下:
详解服务端/客户端|TCP网络通信编程|C++|MFC(上)_第13张图片

3.3 服务端/客户端模型

扣出模型如下:
详解服务端/客户端|TCP网络通信编程|C++|MFC(上)_第14张图片
从运行结果看出,客户端和服务端还不能实现双方互发消息。
接着往下看啦~
由上面的程序慢慢修改即可。

3.4 const char类型的值不能用于初始化char类型的实体

解决: const char类型的值不能用于初始化char类型的实体_触动人生的博客-CSDN博客
将char类型改为const char即可。
在这里插入图片描述

4 一对一收发(非线程)

4.1 一对一通信

简单回顾以下服务端和客户端的创建步骤。
→创建一个服务端程序步骤如下:前4步放在主函数,因为服务器要一直处于监听状态。
① 创建套接字(socket)
② 填写sockaddr_in结构体
③ bind()函数绑定套接字
④ listen()函数监听,设定最大连接数
⑤ accept()函数接收客户端连接请求,返回对应此次连接的套接字
⑥ 用返回的套接字和客户端进行通信(send/recv)
⑦ ……返回另一客户端请求
⑧ 关闭套接字
→创建一个客户端程序步骤如下:
(1)创建套接字(socket)
(2)填写sockaddr_in结构体
(3)connect()函数向服务端发连接请求
(4)send()和recv()函数与服务器通信
(5)关闭套接字

4.2 代码如下:

4.2.1 客户端代码

//【recv】https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-recv
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define WIN32_LEAN_AND_MEAN

#include 
#include 
#include 
#include 
#include 

// Link with ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

#define DEFAULT_BUFLEN 512

int main(int argc, char* argv[]) {

	
	// Declare and initialize variables.
	//const char *sendbuf = "this is a test";

	char sendbuf[DEFAULT_BUFLEN];
	int sendbuflen = DEFAULT_BUFLEN;
	char recvbuf[DEFAULT_BUFLEN];
	int recvbuflen = DEFAULT_BUFLEN;

	// Initialize Winsock
	//==【1】==初始化Winsock,WSAStart()
	WSADATA wsaData;
	int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != NO_ERROR) {
		printf("WSAStartup failed: %d\n", iResult);
		return 1;
	}

	// Create a SOCKET for connecting to server
	//==【2】==socket()函数,创建一个socket
	SOCKET ConnectSocket = INVALID_SOCKET;
	ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (ConnectSocket == INVALID_SOCKET) {
		printf("Error at socket(): %ld\n", WSAGetLastError());
		WSACleanup();
		return 1;
	}

	// The sockaddr_in structure specifies the address family, IP address, and port of the server to be connected to.
	//==【3】==向sockaddr_in结构体填写信息
	struct sockaddr_in clientService;
	std::string serverIP = "127.0.0.1";
	unsigned short serverPORT = 666;

	clientService.sin_family = AF_INET;
	clientService.sin_addr.s_addr = inet_addr(serverIP.c_str());
	clientService.sin_port = htons(serverPORT);

	// Connect to server.
	//==【4】==connect()函数,连接server
	iResult = connect(ConnectSocket, (SOCKADDR*)&clientService, sizeof(clientService));
	if (iResult == SOCKET_ERROR) {
		closesocket(ConnectSocket);
		printf("Unable to connect to server: %ld\n", WSAGetLastError());
		WSACleanup();
		return 1;
	}

	//==【5】==发送接收数据
	while (1) {
		printf("请输入要发送的信息:");
		fgets(sendbuf, DEFAULT_BUFLEN, stdin);	//std::cin>>sednbuf
		iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
		if (iResult == SOCKET_ERROR) {
			printf("send failed: %d\n", WSAGetLastError());
			closesocket(ConnectSocket);
			WSACleanup();
			return 1;
		}
		printf("Bytes Sent: %ld\n", iResult);

		//iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
		iResult = recv(ConnectSocket, recvbuf, (int)strlen(recvbuf), 0);
		recvbuf[iResult] = '\0';	//为字符串增减终结符
		if (iResult > 0)
			std::cout << "服务端信息:" << recvbuf << "Bytes received :" << iResult << "\n ";
		else if (iResult == 0)
			printf("Connection closed\n");
		else
			printf("recv failed: %d\n", WSAGetLastError());

	
	}

	closesocket(ConnectSocket);	//关闭套接字
	WSACleanup();		//释放DLL资源

	return 0;
}

4.2.2 服务端代码

//【accept】https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-accept

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#ifndef UNICODE
#define UNICODE
#endif



#include 
#include 
#include 
#include 
#include 

// Need to link with Ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

#define DEFAULT_BUFLEN 512


int main(int argc, char* argv[])
{
	//初始化
	char sendbuf[DEFAULT_BUFLEN];
	int sendbuflen = DEFAULT_BUFLEN;
	char recvbuf[DEFAULT_BUFLEN];
	int recvbuflen = DEFAULT_BUFLEN;


	// Initialize Winsock.
	//==【1】==初始化Winsock,WSAStartup()函数。
	WSADATA wsaData;
	int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != NO_ERROR) {
		printf("WSAStartup failed with error: %ld\n", iResult);
		return 1;
	}
	
	// Create a SOCKET for listening for incoming connection requests.
	//==【2】==创建socket,socket()函数。
	SOCKET ListenSocket;
	ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (ListenSocket == INVALID_SOCKET) {
		printf("socket failed with error: %ld\n", WSAGetLastError());
		WSACleanup();
		return 1;
	}

	// The sockaddr_in structure specifies the address family, IP address, and port for the socket that is being bound.
	//==【3】==向sockaddr_in结构体写信息。
	sockaddr_in service;
	std::string serverIP = "0.0.0.0";
	unsigned short serverPORT = 666;

	service.sin_family = AF_INET;
	service.sin_addr.s_addr = inet_addr(serverIP.c_str());
	service.sin_port = htons(serverPORT);
	
	//==【4】==绑定socket,bind()函数
	iResult = bind(ListenSocket, (SOCKADDR *)& service, sizeof(service));
	if (iResult == SOCKET_ERROR) {
		printf("bind failed with error: %ld\n", WSAGetLastError());
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}

	// Listen for incoming connection requests. on the created socket
	//==【5】==用listen()函数将ListenSocket置于监听状态,表达这个SOCKET仅仅用于接收客户端连接,而不是用于数据通信;
	iResult = listen(ListenSocket, 6); //6表达这个SOCKET最多允许7个客户端与之同时保持TCP连接;
	if (iResult == SOCKET_ERROR) {
		printf("listen failed with error: %ld\n", WSAGetLastError());
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}

	// Create a SOCKET for accepting incoming requests.	Accept the connection.
	//==【6】==创建连接关系的socket。用accept函数接收客户端连接。
	SOCKET AcceptSocket;
	printf("Waiting for client to connect...\n");
	AcceptSocket = accept(ListenSocket, NULL, NULL);
	if (AcceptSocket == INVALID_SOCKET) {
		printf("accept failed with error: %ld\n", WSAGetLastError());
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}
	else
		printf("Client connected.\n");

	//==【7】==收发数据
	while (1) {
		iResult = recv(AcceptSocket, recvbuf, (int)strlen(recvbuf), 0);
		recvbuf[iResult] = '\0';	为字符串增减终结符
		if (iResult > 0)
			std::cout << "客户端信息:" << recvbuf  << "Bytes received : " << iResult << "\n";
		else if (iResult == 0)
			printf("Connection closed\n");
		else
			printf("recv failed: %d\n", WSAGetLastError());


		printf("请输入要发送的信息:");
		fgets(sendbuf, DEFAULT_BUFLEN, stdin);	//std::cin>>sednbuf
		iResult = send(AcceptSocket, sendbuf, (int)strlen(sendbuf), 0);
		if (iResult == SOCKET_ERROR) {
			printf("send failed: %d\n", WSAGetLastError());
			closesocket(AcceptSocket);
			WSACleanup();
			return 1;
		}
		printf("Bytes Sent: %ld\n", iResult);
	}

	closesocket(ListenSocket);	//关闭套接字
	WSACleanup();				//释放DLL资源

	return 0;
}

4.2.3运行结果如下:

详解服务端/客户端|TCP网络通信编程|C++|MFC(上)_第15张图片

可以看到,服务端和客户端可以相互进行通信。
但是由于将收发消息放在while循环中,所以只能客户端先发一条消息,服务端再回复消息,客户端再发送消息……这样肯定是不行的,如何解决呢?

4.3 【未】字符串终结符’\0’,TCP发送无边界

5 一对多收发(线程)

5.1 线程运行

为了更好的理解线程,学习一下下方的菜鸟教程吧。
线程相关:C++ 多线程 | 菜鸟教程 (runoob.com)

5.2 一对多

如何让服务端和客户端自由收发消息,且服务端能知道是来自哪个客户端的消息。

服务端:将main()函数作为主线程,不断接收客户端的连接请求,即4.1节服务端前4步。再新建子线程,每连接一个客户端,就专为此客户端新建一个用于接收信息并显示到屏幕上功能的子线程。然后,新建子线程,专用于本机发送消息。

客户端:将main()函数作为主线程,连接服务器。新建子线程,用于从服务器接收信息,再新建子线程,用于从客户端向服务器中发送信息。
即:单个服务器的进程利用服务器中的线程与多个客户端进程进行通讯。

5.2.1 程序如下:

5.2.1.1 客户端

//客户端
#define _WINSOCK_DEPRECATED_NO_WARNINGS

//【ErrorCode查询】https://docs.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2
/*-----------------------------------------
客户端:将main()函数作为主线程,连接服务器。
新建子线程,用于从服务器接收信息,
再新建子线程,用于从客户端向服务器中发送信息。
即:单个服务器的进程利用服务器中的线程与多个客户端进程进行通讯。
*/

/*------------------------------------------
(1)创建套接字(socket)
(2)填写sockaddr_in结构体
(3)connect()函数向服务端发连接请求
(4)send()和recv()函数与服务器通信
(5)关闭套接字
*/


#include 
#include 
#include 
#include 
#include 

// Need to link with Ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

using namespace std;

#define DEFAULT_BUFLEN 512
SOCKET ConnectSocket = INVALID_SOCKET; //全局变量,主线程,子线程共享
//char recvbuf[DEFAULT_BUFLEN];
//char sendbuf[DEFAULT_BUFLEN];

DWORD WINAPI SendMessageThread(LPVOID lpParam);
DWORD WINAPI ReceiveMessageThread(LPVOID lpParam);

int main(int argc, char* argv[]) {
	//==【0】==声明和初始化变量
	int iResult;
	WSADATA wsaData;
	char recvbuf[DEFAULT_BUFLEN];
	
	//==【1】==SAStartup()函数初始化Winsock
	iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);	使用2.2版本的Socket。//https://blog.csdn.net/m0_38132603/article/details/78313600
	if (iResult != NO_ERROR) {//如果成功,WSAStartup 函数返回零。否则,它将返回下面列出的错误代码之一。//https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-wsastartup 
		printf("WSAStartup failed with error:%d\n", iResult);
		return 1;
	}

	//==【2】==socket()函数创建套接字
	ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);	//https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
	if (ConnectSocket == INVALID_SOCKET) {
		printf("socket failed with error: %ld\n", WSAGetLastError());
		WSACleanup();	//清理网络环境,释放socket所占的资源
		return 1;
	}

	//==【3】==填写sockaddr_in结构体:IP地址、端口号和协议
	SOCKADDR_IN clientService;
	string serverIP = "127.0.0.3";
	unsigned short serverPORT = 666;

	clientService.sin_family = AF_INET;	//IPv4_https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
	clientService.sin_addr.s_addr = inet_addr(serverIP.c_str());
	clientService.sin_port = htons(serverPORT);

	//==【4】==connect()函数向服务端发送连接请求
	iResult = connect(ConnectSocket, (SOCKADDR*)&clientService, sizeof(clientService));	//https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-connect
	if (iResult == SOCKET_ERROR) {
		printf("connect failed with error: %d\n", WSAGetLastError());
		closesocket(ConnectSocket);		//关闭套接字
		WSACleanup();	//清理网络环境,释放socket所占的资源
		return 1;
	}

	//==【5】==收发线程【butang】去掉while循环===线程运行机制【查看运行起来各个动态】
	//HANDLE mainThread = CreateThread(NULL, 0, SendMessageThread, &ConnectSocket, 0, NULL);
	//HANDLE mainThread = CreateThread(NULL, 0, SendMessageThread, &ConnectSocket, 0, NULL);	//https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createthread	//https://baike.baidu.com/item/CreateThread/8222652#:~:text=CreateThread.%20CreateThread%E6%98%AF%E4%B8%80%E7%A7%8D%E5%BE%AE%E8%BD%AF%E5%9C%A8Windows%20API%E4%B8%AD%E6%8F%90%E4%BE%9B%E4%BA%86%E5%BB%BA%E7%AB%8B%E6%96%B0%E7%9A%84%E7%BA%BF%E7%A8%8B%E7%9A%84%E5%87%BD%E6%95%B0%EF%BC%8C%E8%AF%A5%20%E5%87%BD%E6%95%B0%20%E5%9C%A8%20%E4%B8%BB%E7%BA%BF%E7%A8%8B%20%E7%9A%84%E5%9F%BA%E7%A1%80%E4%B8%8A%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E6%96%B0%E7%BA%BF%E7%A8%8B%E3%80%82.,%E7%BA%BF%E7%A8%8B%20%E7%BB%88%E6%AD%A2%E8%BF%90%E8%A1%8C%E5%90%8E%EF%BC%8C%E7%BA%BF%E7%A8%8B%E5%AF%B9%E8%B1%A1%E4%BB%8D%E7%84%B6%E5%9C%A8%E7%B3%BB%E7%BB%9F%E4%B8%AD%EF%BC%8C%E5%BF%85%E9%A1%BB%E9%80%9A%E8%BF%87CloseHandle%E5%87%BD%E6%95%B0%E6%9D%A5%E5%85%B3%E9%97%AD%E8%AF%A5%E7%BA%BF%E7%A8%8B%E5%AF%B9%E8%B1%A1%E3%80%82.%20%E9%9C%80%E8%A6%81%E8%B0%83%E7%94%A8%E5%88%B0CRT%E5%BA%93%E6%97%B6%EF%BC%8C%E4%B8%8D%E8%A6%81%E7%94%A8CreateThread%20%E5%88%9B%E5%BB%BA%E7%BA%BF%E7%A8%8B%E3%80%81%E5%B9%B6%E7%94%A8CloseHandle%E6%9D%A5%E5%85%B3%E9%97%AD%E8%BF%99%E4%B8%AA%E7%BA%BF%E7%A8%8B%EF%BC%8C%E8%80%8C%E5%BA%94%E8%AF%A5%E7%94%A8_beginthread%E6%9D%A5%E5%88%9B%E5%BB%BA%E7%BA%BF%E7%A8%8B%EF%BC%8C_endthread%E6%9D%A5%E9%94%80%E6%AF%81%E7%BA%BF%E7%A8%8B%E3%80%82.%20%E5%9B%A0%E4%B8%BA%E6%B2%A1%E6%9C%89%E5%AF%B9%E5%AD%90%E7%BA%BF%E7%A8%8B%E4%B8%BACRT%E5%BA%93%E5%88%86%E9%85%8D%E5%A0%86%EF%BC%8C%E4%BC%9A%E5%AF%BC%E8%87%B4%E4%BD%8E%E5%86%85%E5%AD%98%E9%94%99%E8%AF%AF%E8%80%8C%E5%B4%A9%E6%BA%83%E3%80%82.%20CreateThread%20%E4%B8%8D%E4%BC%9A%E5%88%A4%E6%96%ADlpStartAddr%E6%98%AF%E6%95%B0%E6%8D%AE%E8%BF%98%E6%98%AF%E4%BB%A3%E7%A0%81%EF%BC%8C%E7%94%9A%E8%87%B3%E4%B8%8D%E4%BC%9A%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E6%9C%89%E8%B6%B3%E5%A4%9F%E7%9A%84%E8%AE%BF%E9%97%AE%E6%9D%83%E9%99%90%E3%80%82.
	//if (mainThread == NULL) {
	//	printf("CreateThread failed with error: %d\n", GetLastError());
	//	closesocket(ConnectSocket);		//关闭套接字
	//	WSACleanup();	//清理网络环境,释放socket所占的资源
	//	return 1;
	//}
	HANDLE receiveThread = CreateThread(NULL, 0, ReceiveMessageThread, NULL, 0, NULL);

	while (1) {//发送信息放在这个main函数里,也可以放在SendMessageThread里,但要保证while循环在,不然进程就结束了。
		printf("main#001:请输入要发送的消息:\n");
		char sendbuf[DEFAULT_BUFLEN];
		fgets(sendbuf, DEFAULT_BUFLEN, stdin);
		sendbuf[strlen(sendbuf) - 1] = '\0';	//【butang】
		if (strlen(sendbuf) == 1 && sendbuf[0] == 'q') {	//客户端输入q退出
			closesocket(ConnectSocket);
			break;
		}
		std::string sendSTR = sendbuf;
		int rtn = send(ConnectSocket, sendSTR.c_str(), sendSTR.length(), 0);	//通过TCP发送消息;send函数是非阻塞的;
		if (rtn == SOCKET_ERROR)
		{
			printf("main#002:send函数调用失败,错误码=%d;\n", GetLastError());
			closesocket(ConnectSocket);
			break;	//发送失败,意味着TCP连接关系已经不存在,终止while循环;
		}

	}


	//==【6】==关闭socket
	iResult = closesocket(ConnectSocket);
	if (iResult == SOCKET_ERROR) {
		printf("close failed with error: %d\n", WSAGetLastError());
		WSACleanup();
		return 1;
	}

	WSACleanup();
	return 0;
}

//==【发送信息线程】==
DWORD WINAPI SendMessageThread(LPVOID lpParam) {
	//SOCKET uSOCKET = *(SOCKET *)lpParam;		//通过指针传递的SOCKET; 在线程中,我们实际使用的是全局变量;
	//printf("#001:SendMessageThread线程开始了,uSOCKET = [%d]……\n", uSOCKET);

	//printf("snd#001:线程开始了,ConnectSocket = [%d]……\n", ConnectSocket);


	while (ConnectSocket != INVALID_SOCKET) {
		printf("snd#002:请输入要发送的消息:\n");
		char sendbuf[DEFAULT_BUFLEN];
		fgets(sendbuf, DEFAULT_BUFLEN, stdin);
		sendbuf[strlen(sendbuf) - 1] = '\0';	//【butang】
		if (strlen(sendbuf) == 1 && sendbuf[0] == 'q') {
			closesocket(ConnectSocket);
			break;
		}
		std::string sendSTR = sendbuf;
		int rtn = send(ConnectSocket, sendSTR.c_str(), sendSTR.length(), 0);	//通过TCP发送消息;send函数是非阻塞的;
		if (rtn == SOCKET_ERROR)
		{
			printf("snd#003:send函数调用失败,错误码=%d;\n", GetLastError());
			closesocket(ConnectSocket);
			break;	//发送失败,意味着TCP连接关系已经不存在,终止while循环;
		}
	}//while

	closesocket(ConnectSocket);
	ConnectSocket = INVALID_SOCKET;
	//printf("sndThread#004: 线程结束了..........\n");
	return 0;
}

//==【接收信息线程】==
DWORD WINAPI ReceiveMessageThread(LPVOID lpParam) {
	//printf("recv#001:线程开始了,ConnectSocket = [%d]……\n", ConnectSocket);

	while (ConnectSocket != INVALID_SOCKET) {
		char rcvBuf[DEFAULT_BUFLEN];
		//printf("recvThread#002:等待接收TCP消息:\n");	//【butang】做提示用
	//==[R01]==接收TCP消息:recv函数是阻塞的,即,没有收到消息或没有TCP事件时,这个函数不会往下执行:
		int rtn = recv(ConnectSocket, rcvBuf, sizeof(rcvBuf) - 1, 0);	//【butang终结符\0】
		if (rtn == 0) {
			printf("recv#003:通过调用closesocket函数关闭了TCP连接\n");
			break; //此时TCP连接关系已经不存在了,终止while循环
		}
		else if(rtn == SOCKET_ERROR) {
			printf("recv#004:rtn = SOCKET_ERROR; TCP连接出错(各种意外因素,如程序线程被杀死、网络通信中断等等),错误码=%d\n", GetLastError());
			break;	//此时TCP连接关系已经不存在了,终止while循环。检查错误码含义
		}
	//==[R02]==TCP消息显示:此时rtn必然大于0;我们假设接收的都是可见字符;
		rcvBuf[rtn] = '\0';	//为字符串增加终结符【butang】
		//printf("recv#005:Server says:%s,==[%d]字节==\n", rcvBuf,rtn);
		printf("Server says: %s\n", rcvBuf);     // 接收信息  

	}//while
	
	//closesocket(ConnectSocket);
	//ConnectSocket = INVALID_SOCKET;
	//printf("recv#006: 线程结束了..........\n");
	return 0;
}

5.2.1.2 服务端

//服务端
//【ErrorCode查询】https://docs.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2

/*--------------------------------------
服务端:将main()函数作为主线程,不断接收客户端的连接请求,即4.1节服务端前4步。
再新建子线程,每连接一个客户端,就专为此客户端新建一个用于接收信息并显示到屏幕上功能的子线程。
然后,新建子线程,专用于本机发送消息。
*/
/*-------------------------------------
① 创建套接字(socket)
② 填写sockaddr_in结构体
③ bind()函数绑定套接字
④ listen()函数监听,设定最大连接数
⑤ accept()函数接收客户端连接请求,返回对应此次连接的套接字
⑥ 用返回的套接字和客户端进行通信(send/recv)
⑦ ……返回另一客户端请求
⑧ 关闭套接字
*/

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS

#ifndef UNICODE
#define UNICODE
#endif



#include 
#include 
#include 
#include 
#include 

#include   
#include 
#include 
#include 
#include 
#include 

using namespace std;

// Need to link with Ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

vector <SOCKET> clientSocketGroup;//【https://www.cnblogs.com/Fightingbirds/p/3934264.html】
#define DEFAULT_BUFLEN 512
SOCKET AcceptSocket;


DWORD WINAPI ReceiveMessageThread(LPVOID lpParam);
DWORD WINAPI SendMessageThread(LPVOID IpParameter);

// ==>>声明一个结构体。用于向服务器线程传递客户端的相关信息
typedef struct client_info {
	SOCKET cSocket;
	char IP[128];
	unsigned short Port;
}CLIENT_INFO;

int main(int argc, char* argv[]) {
	//==初始化
	WSADATA wsaData;
	/*char sendbuf[DEFAULT_BUFLEN];
	int sendbuflen = DEFAULT_BUFLEN;
	char recvbuf[DEFAULT_BUFLEN];
	int recvbuflen = DEFAULT_BUFLEN;*/

	//==【1】==WSAStartup()函数初始化Winsock
	int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != NO_ERROR) {
		printf("WSAStartup failed with error: %ld\n", iResult);
		return 1;
	}

	//==【2】==socket()函数
	SOCKET ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (ListenSocket == INVALID_SOCKET) {
		printf("socket failed with error: %ld\n", WSAGetLastError());
		WSACleanup();
		return 1;
	}

	//==【3】==填写sockaddr_in结构体
	SOCKADDR_IN service;
	SOCKADDR_IN clientAddr; //存储客户端信息
	CLIENT_INFO cINFO;	//存储客户端socket、port、ip信息
	std::string serverIP = "0.0.0.0";
	unsigned short serverPORT = 666;

	service.sin_family = AF_INET;
	service.sin_addr.s_addr = inet_addr(serverIP.c_str());
	service.sin_port = htons(serverPORT);

	//==【4】==bind()函数绑定套接字
	iResult = bind(ListenSocket, (SOCKADDR *)& service, sizeof(service));
	if (iResult == SOCKET_ERROR) {
		printf("bind failed with error: %ld\n", WSAGetLastError());
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}
	
	//==【5】==listen()函数监听,设定最大连接数
	iResult = listen(ListenSocket, 6); //6表达这个SOCKET最多允许7个客户端与之同时保持TCP连接;
	if (iResult == SOCKET_ERROR) {
		printf("listen failed with error: %ld\n", WSAGetLastError());
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}

	//==【6】==accept()函数接收客户端连接请求,返回对应此次连接的套接字
	//HANDLE sendThread = CreateThread(NULL, 0, SendMessageThread, NULL, 0, NULL);

	while (1) {//不断等待客户端请求的到来
		int ClientAddrBytes = sizeof(sockaddr_in);
		AcceptSocket = accept(ListenSocket, (sockaddr *)&clientAddr, &ClientAddrBytes);
		if (AcceptSocket == INVALID_SOCKET) {
			printf("accept failed with error: %ld\n", WSAGetLastError());
			closesocket(ListenSocket);
			WSACleanup();
			return 1;
		}
		else {
			printf("Client connected.\n");
			clientSocketGroup.push_back(AcceptSocket);	//记录对应此次链接的套接字
		}
			
		//===>> 显示客户端的IP和端口号:注意如何将sockaddr_in结构体中的IP转换为字符串IP,如何将端口号转换为主机字节顺序;
		printf("main#001: 收到客户端TCP连接:%s:%d\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
		cINFO.cSocket = AcceptSocket;
		strncpy(cINFO.IP, inet_ntoa(clientAddr.sin_addr), 16);
		cINFO.Port = ntohs(clientAddr.sin_port);

		//==>> 计数器:统计有多少个服务线程再运行中;
		//int ClientThreadCnt = 0;
		//===>>显示仍保持连接的客户端数量
		//ClientThreadCnt++;
		//printf("main#002: 仍有%d个客户端服务线程在运行\n", ClientThreadCnt);
		printf("main#002: 仍有[%d]个客户端服务线程在运行\n", clientSocketGroup.size());

		
		//===>>服务端收/发消息
		CLIENT_INFO *pNewClient = new CLIENT_INFO;
		memcpy(pNewClient, &cINFO, sizeof(CLIENT_INFO));
		HANDLE receiveThread = CreateThread(NULL, 0, ReceiveMessageThread, pNewClient, 0, NULL);
		if (receiveThread == NULL){
			printf("error#174:创建线程失败,错误码=%d; 按下回车退出!\n", GetLastError());
			closesocket(ListenSocket);
			return -1;
		}
		HANDLE sendThread = CreateThread(NULL, 0, SendMessageThread, NULL, 0, NULL);
	}//while

	WSACleanup();
	return 0;
}

DWORD WINAPI ReceiveMessageThread(LPVOID lpParam) {
	
	//printf("recv#001: 线程开始了..........\n");
	char recvbuf[DEFAULT_BUFLEN];


	CLIENT_INFO uCLIENT;
	memcpy(&uCLIENT, lpParam, sizeof(CLIENT_INFO));
	delete lpParam;

	while (uCLIENT.cSocket != INVALID_SOCKET) {
		//printf("recv#002:等待收TCP消息……\n");
		int  rtn = recv(uCLIENT.cSocket, recvbuf, sizeof(recvbuf)-1, 0);

		//for (int i = 0; i < clientSocketGroup.size(); ++i) {
		//	std::string sndSTR = sendbuf;
		//	//int  rtn = recv(clientSocketGroup[i], recvbuf, sizeof(recvbuf) - 1, 0);
		//	//send(clientSocketGroup[i], sndSTR.c_str(), DEFAULT_BUFLEN, 0);   // 发送信息  
		//}

		if (rtn == 0){
			printf("recv#003: Recv线程:rtn = 0; 表达对端优雅地关闭了TCP连接(通过调用closesocket函数)!\n");
			break; //此时TCP连接关系已经不存在了,终止while循环
		}
		else if (rtn == SOCKET_ERROR){
			printf("recv#004: Recv线程:rtn = SOCKET_ERROR; TCP连接出错(各种意外因素,如程序线程被杀死、网络通信中断等等),错误码=%d\n", GetLastError());
			break; //此时TCP连接关系已经不存在了,终止while循环
		}

		recvbuf[rtn] = '\0';	//字符串终结符
		//printf("recv#005: Recv线程:%s:%d消息[%d]字节 =========>>>\n", uCLIENT.IP, uCLIENT.Port, rtn);
		//printf(" <<<=========info#79: Recv线程:%s:%d消息[%d]字节 =========>>>\n", uCLIENT.IP, uCLIENT.Port, rtn);
		printf("Client says: %s,==%s:%d消息[%d]字节==\n", recvbuf, uCLIENT.IP, uCLIENT.Port, rtn);
	}//while

	closesocket(uCLIENT.cSocket);
	uCLIENT.cSocket = INVALID_SOCKET;
	//printf("recv#005: 线程结束了..........\n");

	return 0;
}


DWORD WINAPI SendMessageThread(LPVOID IpParameter) {

	while (1) {
		//printf("snd#001:线程开始了……\n");
		char sendbuf[DEFAULT_BUFLEN];
	
		fgets(sendbuf, DEFAULT_BUFLEN, stdin);
		//printf("snd#002:服务端发送信息为;%s\n", sendbuf);

		for (int i = 0; i < clientSocketGroup.size(); ++i) {
			std::string sndSTR = sendbuf;
			send(clientSocketGroup[i], sndSTR.c_str(), DEFAULT_BUFLEN, 0);   // 发送信息  
		}
	}//while
	//printf("snd#003:线程结束了……\n");

	return 0;
}

5.1.2.3 运行结果

运行结果如下:
详解服务端/客户端|TCP网络通信编程|C++|MFC(上)_第16张图片
从上图可以看出,客户端会反复说:Server says:,在另一组通信中却不会(后期再改),如下:
详解服务端/客户端|TCP网络通信编程|C++|MFC(上)_第17张图片
下一步:服务端不仅能广播消息给每个客户端,还能发送信息给某个指定的客户端。一次发出的消息超过DEFAULT_BUFFER时,会发生什么呢?能正确接收到消息吗?

参考文献【未贴完,今天寝室要关门了】

  1. 导师的课程代码及MSDN官方代码
  2. https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-connect

你可能感兴趣的:(C++,c++,tcpip,socket)