【C++】TCP通信服务端与客户端代码实现及详解

一、服务端server实现

1. server代码

#include <WinSock2.h>
#include <WS2tcpip.h>
#include <iostream>
#include <string>
#include <thread>

#pragma comment(lib, "ws2_32.lib")
#define PORT 65432

void Cleanup(SOCKET socket)
{
    
    
	closesocket(socket);
	WSACleanup();
}

void HandleClientCommunication(SOCKET clientSocket)
{
    
    
	std::cout << "成功和" << clientSocket << "建立连接!" << std::endl;

	while (true)
	{
    
    
		char buffer[1000];
		std::cout << "服务器向" << clientSocket << "发送数据:" << std::endl;
		std::cin.getline(buffer, sizeof(buffer));

		std::string message = buffer;
		int size = send(clientSocket, message.c_str(), static_cast<int>(message.length()), 0);
		if (message == "end")
		{
    
    
			std::cout << "关闭和" << clientSocket << "的连接!" << std::endl;
			break;
		}
		else if (size == SOCKET_ERROR || size == 0)
		{
    
    
			std::cout << "[发送信息失败] 错误代码:" << WSAGetLastError() << std::endl;
			break;
		}
		else
		{
    
    
			std::cout << "       [信息发送成功]" << std::endl;
		}

		memset(buffer, 0, sizeof(buffer));
		int receivedBytes = recv(clientSocket, buffer, sizeof(buffer), 0);
		if (receivedBytes == SOCKET_ERROR || receivedBytes == 0)
		{
    
    
			std::cout << clientSocket << "断开了连接!" << std::endl;
			break;
		}
		else
		{
    
    
			std::cout << clientSocket << "  说: " << buffer << std::endl;
		}
	}
	closesocket(clientSocket);
}

int main()
{
    
    
	WSADATA wsaData;
	WORD wVersionRequested = MAKEWORD(2, 2);
	if (WSAStartup(wVersionRequested, &wsaData) != 0)
	{
    
    
		std::cout << "加载winsock.dll失败!" << std::endl;
		return 0;
	}

	SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, 0);
	if (serverSocket == INVALID_SOCKET)
	{
    
    
		std::cout << "创建套接字失败!错误代码:" << WSAGetLastError() << std::endl;
		WSACleanup();
		return 0;
	}

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

	if (bind(serverSocket, reinterpret_cast<SOCKADDR*>(&serverAddress), sizeof(serverAddress)) == SOCKET_ERROR)
	{
    
    
		std::cout << "地址绑定失败!错误代码:" << WSAGetLastError() << std::endl;
		Cleanup(serverSocket);
		return 0;
	}

	if (listen(serverSocket, 0) == SOCKET_ERROR)
	{
    
    
		std::cout << "监听失败!错误代码:" << WSAGetLastError() << std::endl;
		Cleanup(serverSocket);
		return 0;
	}

	while (true)
	{
    
    
		sockaddr_in clientAddress;
		int clientAddressSize = sizeof(clientAddress);
		SOCKET clientSocket = accept(serverSocket, reinterpret_cast<SOCKADDR*>(&clientAddress), &clientAddressSize);
		if (clientSocket != INVALID_SOCKET)
		{
    
    
			std::thread clientThread(HandleClientCommunication, clientSocket);
			clientThread.detach(); // 分离线程,不等待线程结束
		}
	}

	Cleanup(serverSocket);
	return 0;
}

2. 代码详解

上述代码使用Winsock库实现了简单的TCP服务器,它监听指定端口并与客户端进行通信。下面对代码进行详细分析:

  1. #pragma comment(lib, "ws2_32.lib") 是一个特殊的编译器指令,用于告诉编译器在链接阶段将 ws2_32.lib 库文件添加到最终的可执行文件中。无需在编译命令行或IDE中显式指定。这样做的好处是,可以将库文件的链接过程自动化,减少了手动操作的繁琐性,并确保在编译和链接过程中正确地使用所需的库文件。需要注意的是,该指令是特定于Microsoft Visual C++编译器的,并不是标准C++的一部分,因此在使用其他编译器时可能需要采用不同的方式来链接相应的库文件。

  2. Cleanup函数:该函数用于关闭套接字清理WSA(Winsock库)的资源。在程序退出前,应该调用该函数来进行善后处理。

    void Cleanup(SOCKET socket)
    {
          
          
    	closesocket(socket);   //关闭套接字
    	WSACleanup();          //清理WSA的资源
    }
    
  3. HandleClientCommunication函数:该函数在单独的线程中处理与客户端的通信。它首先向客户端发送数据,然后接收客户端的响应。如果接收到的消息是"end",表示客户端要求关闭连接,函数会退出循环并关闭套接字。

  4. main函数:程序的入口函数。它首先初始化Winsock库,创建一个套接字并绑定到指定的IP地址和端口。然后,服务器开始监听客户端连接请求。当有客户端连接时,将创建一个新的线程来处理与该客户端的通信。使用detach函数将线程分离,不等待线程结束。

  5. WSAStartup:初始化Ws2_32.dll动态链接库,指定winsock2.2版本,在使用socket之前,一定要初始化该链接库。。

    WSADATA wsaData;
    WORD wVersionRequested = MAKEWORD(2, 2);
    if (WSAStartup(wVersionRequested, &wsaData) != 0)
    {
          
          
    	std::cout << "加载winsock.dll失败!" << std::endl;
    	return 0;
    }
    
  6. socket:创建套接字,指定地址族(AF_INET)、套接字类型(SOCK_STREAM)和协议(0表示根据地址族和套接字类型自动选择)。

    SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, 0);
    
  7. bind:将套接字绑定到指定的IP地址和端口。第一个参数为socket,第二个参数是一个结构指针,它包含了端口和IP地址信息,第三个参数表示缓冲区长度。

    sockaddr_in serverAddress;
    serverAddress.sin_family = AF_INET;
    serverAddress.sin_port = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &serverAddress.sin_addr);
    bind(serverSocket, reinterpret_cast<SOCKADDR*>(&serverAddress), sizeof(serverAddress));
    
  8. inet_pton()函数用于将点分十进制的IP地址转换为网络字节序的二进制形式。并将结果存储在serverAddress.sin_addr中。

    注意:inet_pton()函数是一个标准的C函数,位于头文件arpa/inet.h中。它返回一个整数值,指示转换的结果是否成功。如果返回值为1,表示转换成功;如果返回值为0,表示输入的IP地址无效;如果返回值为-1,表示发生了错误,可以使用errno变量获取具体的错误信息。

  9. htons:将一个16位无符号短整型数据由主机排列方式转化为网络排列方式,htonl函数的作用相反。

    serverAddress.sin_port = htons(PORT);
    
  10. listen:开始监听客户端连接请求。第一个参数为socket,第二个参数为等待连接最大队列的长度。将socket设置为监听模式,服务端的socket特有。必须将服务端的socket设置为监听模式才能和服务端建立连接。

    listen(serverSocket, 0);
    
  11. accept:服务端socket接受客户端的连接请求,返回一个新的套接字以用于与客户端通信。第一个参数为socket,第二个参数为包含客户端端口IP信息的sockaddr_in结构指针,第三个参数为接收参数addr的长度。

sockaddr_in clientAddress;
int clientAddressSize = sizeof(clientAddress);
SOCKET clientSocket = accept(serverSocket, reinterpret_cast<SOCKADDR*>(&clientAddress), &clientAddressSize);
  1. std::thread:使用C++11的std::thread库创建新的线程,将HandleClientCommunication函数作为线程的入口点,并传递客户端套接字作为参数。

  2. recv接收数据,第一个参数为socket,第二个参数为接收数据缓冲区,第三个参数为缓冲区的长度,第四个参数为函数的调用方式。

    int receivedBytes = recv(clientSocket, buffer, sizeof(buffer), 0);
    
  3. send发送数据,里面的参数基本和recv()一样。

    int size = send(clientSocket, message.c_str(), static_cast<int>(message.length()), 0);
    

以上是对服务端代码的详细分析。代码实现了一个简单的TCP服务器,能够与多个客户端进行通信。代码仍然有改进的空间,例如可以处理更多的错误情况,添加更多的日志输出,以及更完善的内存管理等。


二、客户端client实现

1. client代码

#include <winsock2.h>
#include <WS2tcpip.h>
#include <iostream>
#include <string>

#pragma comment(lib, "ws2_32.lib")
#define PORT 65432

void Cleanup(SOCKET socket)
{
    
    
	closesocket(socket);
	WSACleanup();
}

int main()
{
    
    
	WSADATA wsaData;
	WORD wVersionRequested = MAKEWORD(2, 2);
	if (WSAStartup(wVersionRequested, &wsaData) != 0)
	{
    
    
		std::cout << "加载winsock.dll失败!" << std::endl;
		return 0;
	}

	SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, 0);
	if (clientSocket == INVALID_SOCKET)
	{
    
    
		std::cout << "创建套接字失败!错误代码:" << WSAGetLastError() << std::endl;
		Cleanup(clientSocket);
		return 0;
	}

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

	if (connect(clientSocket, reinterpret_cast<SOCKADDR*>(&serverAddress), sizeof(serverAddress)) == SOCKET_ERROR)
	{
    
    
		std::cout << "连接失败!错误代码:" << WSAGetLastError() << std::endl;
		Cleanup(clientSocket);
		return 0;
	}

	while (true)
	{
    
    
		std::string receivedMessage;
		char buffer[1000];
		int size = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
		if (size <= 0)
		{
    
    
			if (size == 0)
				std::cout << "连接已关闭" << std::endl;
			else
				std::cout << "接收信息失败!错误代码:" << WSAGetLastError() << std::endl;
			break;
		}
		buffer[size] = '\0';
		receivedMessage = buffer;
		if (receivedMessage == "end")
		{
    
    
			std::cout << "服务器端已关闭连接!" << std::endl;
			break;
		}
		else
		{
    
    
			std::cout << "收到服务器消息:" << receivedMessage << std::endl;
		}

		std::string sendMessage;
		std::cout << "请输入要发送给服务器的消息:" << std::endl;
		std::getline(std::cin, sendMessage);

		if (sendMessage == "end")
		{
    
    
			std::cout << "关闭连接!" << std::endl;
			break;
		}

		int sentBytes = send(clientSocket, sendMessage.c_str(), static_cast<int>(sendMessage.length()), 0);
		if (sentBytes == SOCKET_ERROR || sentBytes == 0)
		{
    
    
			std::cout << "[发送信息失败] 错误代码:" << WSAGetLastError() << std::endl;
			break;
		}
		else
		{
    
    
			std::cout << "       [信息发送成功]" << std::endl;
		}
	}

	Cleanup(clientSocket);
	return 0;
}

2. 代码详解

上述代码是一个使用Winsock库实现的简单的TCP客户端,它连接到指定的服务器并与之进行通信。下面对代码进行详细分析:

  1. Cleanup函数:与之前的代码相同,用于关闭套接字并清理WSA的资源。

  2. main函数:程序的入口函数。它首先初始化Winsock库,然后创建一个套接字并尝试连接到服务器。接下来,进入一个无限循环,循环中首先接收服务器发送的消息,然后等待用户输入要发送给服务器的消息。如果接收到的消息是"end",表示服务器要求关闭连接,循环会退出。如果用户输入的消息是"end",表示用户主动要求关闭连接,循环也会退出。

  3. WSAStartup:与之前的代码相同,初始化Winsock库。

  4. socket:与之前的代码相同,创建套接字。

  5. connect:客户端socket发送连接请求的函数,尝试连接到指定的服务器地址和端口。第一个参数是客户端的socket,第二个参数是一个结构体指针,里面包括连接主机的地址和ip,第三个参数为缓冲区的长度。

    connect(clientSocket, reinterpret_cast<SOCKADDR*>(&serverAddress), sizeof(serverAddress));
    
  6. recv:接收服务器发送的消息,将其存储在buffer中,然后通过对buffer进行处理得到receivedMessage

  7. send:将用户输入的消息发送给服务器。

以上是客户端代码的详细分析。代码实现了一个简单的TCP客户端,能够与服务器进行通信。代码仍然有改进的空间,例如可以处理更多的错误情况,添加更多的日志输出,以及更完善的输入验证等。

三、运行效果图

服务端与客户端通信效果图

四、总结

  • 服务端有一个ServerSocket,初始化以后(设置IP和端口,绑定监听等),使用accept()方法等待客户端连接,这个方法是阻塞的。一旦连接成功,会返回一个新的Socket,使用这个Socket就可以接收数据和发送数据了。

  • 客户端自始始终都只有一个Socket,这个Socket初始化以后,使用connect()方法和服务器进行连接,连接成功后,这个Socket就可以进行通信了。


如果这篇文章对你有所帮助,渴望获得你的一个点赞!

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/AAADiao/article/details/133924535