一.思路
首先是进行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;
}
}
}
最后运行,先跑服务器,再跑客户端。