Bootstrap

C++基于TCP的SOCKET通信

一.思路

首先是进行SOCKET通信,这里我们是通过公网IP进行通信的,所以服务端要使用服务器,然后如果我们在输入内容的是否有消息来到,就要使用多线程,思路就是这样,然后是代码实现。

二.服务器

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

这里是C++SOCKET通信的一个头文件和一个库文件

WSADATA wsaData = { 0 };
WSAStartup(MAKEWORD(2, 2), &wsaData);

这个是C++ #include <WinSock2.h>的初始化,只有使用了这两行代码以后才可以使用相应的API。然后说明一下网络通信里有一个通用的错误代码调试:WSAGetLastError。

注意这两行代码是不可以使用WSAGetLastError的。

因为WSAGetLastError也是一个对应的API,但是只有用了这两行以后,才可以使用对应的API,所以如果这两行代码运行时出现了错误,对应的API就不能使用,所以WSAGetLastError也就不能使用了。

SOCKET server_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

然后是创建套接字。那么套接字是什么东西呢?

首先我觉得这个翻译确实十分的神奇,socket还有一个翻译叫做插座,这个就形象多了。这个socket里包含呢通信协议,IP地址以及端口号。这里就可以明确信息具体要发送到哪里。

首先是通信协议,就是tcp/ip协议, 这里使用的是TCP协议

然后是IP地址,这里可以是公网IP,也可以是内网IP,公网IP的话就要使用服务器,因为自己的电脑一般是不能被外望访问到的,用公网IP的话就可以随意访问了,比如在新疆可以访问到杭州的服务器。当然也可以使用内网IP,也就是192.168开头的IP地址,这里的话服务器就可以使用自己的电脑了,但是只有在同一个WIFI内才能使用。

最后就是端口号。在上面的叙述中,我们已经通过IP地址找到了我们要发送给具体那一台电脑,然后就是端口号。端口号就好比一个电脑的入口有好多好多的入口,并且标了号。比如微信使用了1号端口,QQ使用的是2号端口,如果这两个端口混在一起使用,就肯定会出问题,我们填写了端口号,就可以具体确认信息要发送到哪一个程序。这里要注意1024号端口以前的端口可能会和系统端口冲突,所以最好使用8888,8080之类的端口。

这里讲解了socket具体是个什么东西,然后来解释一下上面的一行代码。

首先创建了一个名叫server_socket的SOCKET套接字,然后我们用socket函数赋值。

第一个参数是协议家族,可以填AF_INET或者PF_INET,第一个是address family(地址家族),第二个是protocol family(协议家族),这两个理论上来讲是一样的,但是因为这里是协议,所以使用协议家族,也就是PF_INET.

第二个参数是通信协议,可以填写SOCK_STREAM或者SOCK_DGRAM。我们这里填写SOCK_SREAM。第一个SOCK_STREAM意思是TCP通信,SOCK_DGRAM是UDP通信。我们这里使用TCP通信。那么这两种通信协议有什么区别呢?通俗的讲,TCP是比较保险的,不会有数据的丢失,而且是按照顺序的,一般用于文字传输,但是速度比较慢。UDP是不太保险的,有可能会造成数据丢失,但是由于速度较快,一般可以用于实时视频传输,因为我们只是传输文字,所以使用TCP,当然在实际中,大家可以更改这个参数。

第三个参数也没什么好解释的,第二个用了TCP的,第三个就用IPPROTO_TCP,第二个用了UDP的,第三个就用IPPROTO_UDP,如果大家觉得这个第三个参数麻烦,可以填写NULL,程序就会根据第二个参数来调节。

struct sockaddr_in server_addr;
server_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);

这里是把我们的数据填充到一个结构体中

我们刚才说了socket要协议,端口,IP这些参数,但是刚才是没有配置这些参数的,我们这里就把这些参数先填充到一个结构体当中,接下来解读一下:
第一行:就是建立了一个结构体,用于存放数据

第二行:填充IP,htonl可以翻译为:host to network long,也就是本机到网络并使用大尾存储,因为我们的服务器有可能在这台电脑上运行,有可能在别的电脑上运行,无法确定服务器的IP,所以使用INADDR_ANY,这个会自动调节IP

这里也可以指定IP,可使用:

server_addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

IP可以用循环地址,也可以用内网IP。

注意: 使用inet_addr的情况下注意,在vs2022下会报错,原因是inet_addr是比较老的函数,解决报错方法:项目属性 -> C/C++ -> SDL检查 -> 关闭SDL检查,也就是后面的参数变成“否”

第三行:地址家族,之前说了,这里是地址有关的,所以填写AF_INET

第四行:端口号,htons可翻译为:host to network short,也就是本机到网络并使用小尾存储,然后是端口号,我这里开的是8888.

(大尾:高地址存储小数据,小尾:高地址存储大数据)

bind(server_socket, (SOCKADDR*)&server_addr, sizeof(server_addr));

参数配置完成以后,就需要把我们的参数和刚刚新建的server_socket这个套接字绑定起来。

第一个参数: 套接字名称

第二个参数:配置参数的结构体,注意要强制类型转换

第三个参数:结构体大小

讲了那么多,我们的套接字终于配置完毕,最后就是开始监听:

listen(server_socket, SOMAXCONN);

这是开始监听。

第一个参数:服务器套接字名称

第二个参数:允许同时接入的客户端个数。也就是说同时可以和几个客户端联系。可以填数字,也可以使用SOMAXCONN,表示最大的可以连接客户端的个数。

sockaddr_in client_addr;
int client_size = sizeof(client_addr);
SOCKET client_socket = accept(server_socket, (SOCKADDR*)&client_addr, &client_size);

开始监听以后,程序就一直会在监听,不会向下走,如果有客户端来连接了,我们就使用这三行代码,总体意思就是把客户端:

第一行:新建一个客户端的配置套接字的结构体,这个结构体和之前讲的服务器的结构体用处是一样的,只不过刚才的server_addr存储服务器相关参数,client_addr存储客户端相关参数

第二行:获取结构体大小

第三方:新建并且配置套接字,这里就使用accept(连接)函数来配置我们的服务器套接字,因为已经监听到有客户端了,这里肯定就要接受客户端连接请求。参数和刚刚讲的bind函数类似,唯一有区别的就是最后一个参数:只能在外面int一个结构体大小再用指针传进来,要不然是错误的,搞不懂的话就照着这个写,肯定没有问题

接下来,我们要接受和发送数据了,这里要使用多线程,主线程用于接受信息,子线程用于发送信息。

void send_all(SOCKET* socket) {
	while (1) {
		server_send_change.clear();
		getline(cin, server_send_change);
		for (int i = 0; i < server_send_change.length(); i++) {
			server_send[i] = server_send_change[i];
		}
		if (send(*socket, server_send, server_send_change.length() + 1 + sizeof(char), 0) == SOCKET_ERROR) {
			cout << "发送失败" << endl;
		}
	}
}

这时一个发送函数,函数不需要返回值,使用void即可。需要一个参数,就是客户端的套接字,因为我们是发送信息给客户端,所以需要客户端的相关信息。

然后先写一个死循环,这个server_send_change是我在头上定义的一个string类型的字符串,首先先用clear清空它,然后用getline读取,再写一个循环存储到char类型的字符串当中,最后使用send函数发送,这里讲一下send函数:

第一个参数:套接字名称(客户端的)

第二个参数:要发送的字符串(char[]类型)

第三个参数:要发送的字符长度加上char类型的大小

第四个参数:写0即可。

 然后这里还有,SOCKET_ERROR,也是判断错误的,如果send的返回值等于SOCKET_ERROR,就输出发送失败。

thread th_send(send_all, &client_socket);
th_send.detach();

紧接着,我们在主函数里面直接调用这个函数,并使用多线程,第一行传参,第二行要使用detach,因为我们程序有死循环堆着,不会直接终止,所以detach不会出现传参问题,不要使用join,要不然会出问题。

while (1) {
		temp = recv(client_socket, server_recv, MAXBYTE, 0);
		if (temp != -1) {
			cout << "收到客户端消息:";
			for (int i = 0; i < strlen(server_recv); i++) {
				cout << server_recv[i];
			}
			cout << endl;
			temp = 0;
		}
	}

最后,我们在主函数里面写接受函数。首先也是一个死循环,然后使用recv函数:

第一个参数:套接字名称

第二个参数:一个char[]类型的字符串,用于存储接受到的字符

第三个参数:最大字符数量,直接填写最大的

第四个参数:填写0即可

然后我们int一个temp,来接受send的返回值,如果temp不等于-1,就说明消息来了,输出就可以了,最后换行,重置temp。

closesocket(server_socket);
WSACleanup();

最后释放网络资源。 

至此,服务器代码完毕,完整代码:

#include <iostream>
#include <WinSock2.h>
#include <string>
#include <conio.h>
#include <thread>
#pragma comment(lib,"Ws2_32.lib")
using namespace std;
char server_recv[MAXBYTE] = { 0 };
char server_send[MAXBYTE] = { 0 };
string server_send_change;
void send_all(SOCKET* socket);
int main() {
	WSADATA wsaData = { 0 };
	WSAStartup(MAKEWORD(2, 2), &wsaData);
	SOCKET server_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	struct sockaddr_in server_addr;
	server_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(8888);
	bind(server_socket, (SOCKADDR*)&server_addr, sizeof(server_addr));
	listen(server_socket, SOMAXCONN);
	sockaddr_in client_addr;
	int client_size = sizeof(client_addr);
	SOCKET client_socket = accept(server_socket, (SOCKADDR*)&client_addr, &client_size);
	cout << "连接成功" << endl;
	cout << "用户信息:" << inet_ntoa(client_addr.sin_addr) << endl;
	int temp = 0;
	thread th_send(send_all, &client_socket);
	th_send.detach();
	while (1) {
		temp = recv(client_socket, server_recv, MAXBYTE, 0);
		if (temp != -1) {
			cout << "收到客户端消息:";
			for (int i = 0; i < strlen(server_recv); i++) {
				cout << server_recv[i];
			}
			cout << endl;
			temp = 0;
		}
	}
	closesocket(server_socket);
	WSACleanup();
	while (1);
	return 0;
}
void send_all(SOCKET* socket) {
	while (1) {
		server_send_change.clear();
		getline(cin, server_send_change);
		for (int i = 0; i < server_send_change.length(); i++) {
			server_send[i] = server_send_change[i];
		}
		if (send(*socket, server_send, server_send_change.length() + 1 + sizeof(char), 0) == SOCKET_ERROR) {
			cout << "发送失败" << endl;
		}
	}
}

三.客户端 

说真的,客户端和服务器代码基本一致,只有两三处修改即可,我只说要更改的,剩下的是一毛一样的:

1.客户端配置

client_addr.sin_addr.S_un.S_addr = inet_addr(addr);

这里的ip不填写本机,填写客户端的公网ip或是内网IP,addr是我之前声明的一个字符串

2.bind和connet 

注意:客户端不用bind!

 客户端不用bind!

客户端不用bind!

客户端不用bind!

 一定要切记

取而代之的是connet函数:

connect(client_socket, (SOCKADDR*)&client_addr, sizeof(client_addr));

参数和bind一样,不加赘述。

然后就基本没有区别了。

客户端代码:

#include <iostream>
#include <WinSock2.h>
#include <string>
#include <conio.h>
#include <thread>
#pragma comment(lib,"Ws2_32.lib")
using namespace std;
char client_recv[MAXBYTE] = { 0 };
char client_send[MAXBYTE] = { 0 };
string client_send_change;
void send_all(SOCKET* socket);
int client_end();
int main() {
	cout << "请输入服务器IP:";
	int k = 0;
	char addr[100];
	cin >> addr;
	WSADATA wsaData = { 0 };
	WSAStartup(MAKEWORD(2, 2), &wsaData);
	SOCKET client_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	struct sockaddr_in client_addr {};
	client_addr.sin_addr.S_un.S_addr = inet_addr(addr);
	client_addr.sin_family = AF_INET;
	client_addr.sin_port = htons(8888);
	connect(client_socket, (SOCKADDR*)&client_addr, sizeof(client_addr));
	int temp = 0;
	thread th_send(send_all, &client_socket);
	th_send.detach();
	while (1) {
		temp = recv(client_socket, client_recv, MAXBYTE, 0);
		if (temp != -1) {
			cout << "收到服务器消息:";
			for (int i = 0; i < strlen(client_recv); i++) {
				cout << client_recv[i];
			}
			cout << endl;
			temp = 0;
		}
	}
	WSACleanup();
	closesocket(client_socket);
	while (1);
	return 0;
}
void send_all(SOCKET* socket) {
	while (1) {
		client_send_change.clear();
		getline(cin, client_send_change);
		for (int i = 0; i < client_send_change.length(); i++) {
			client_send[i] = client_send_change[i];
		}
		if (send(*socket, client_send, client_send_change.length() + 1 + sizeof(char), 0) == SOCKET_ERROR) {
			cout << "发送失败" << endl;
		}
	}
}

最后运行,先跑服务器,再跑客户端。 

 

;