C++网络编程学习:跨平台支持Windows、Linux系统

  • 使用的语言为C/C++
  • 源码支持的平台为:Windows / Linux

一、为何要进行跨平台操作

  首先,我是想在网络编程学习渐入佳境后,自己尝试做一个网络方面的项目,其中就必须用到服务器。Linux服务器相比Windows服务器更加稳定且高效,所以对于我来说,学会如何编写出可以在Linux系统下运行的网络程序是必不可少的。
  其次,就目前来说,企业中的高性能网络编程都是基于Linux的,学会跨平台的网络编程技能,可以在未来就业方面等有很大的好处。
  由此,我决定在网络编程学习的第四小阶段,学习如何进行跨平台的网络编程。

二、关于Win与Linux系统下网络编程的差异

差异一

在Linux环境下,程序的头文件与定义与Win环境下存在差异。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include<winSock2.h>
#include<windows.h>
#pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有
#else
#include<arpa/inet.h>//selcet
#include<unistd.h>//uni std
#include<string.h>

#define SOCKET int
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR (-1)
#endif

这是更改后的程序部分。
可以看出:

  • Win环境下的特有头文件 <windows.h> 对应Linux环境下的特有头文件 **<unistd.h>**。
  • Win环境下的网络头文件 <winSock2.h> 对应Linux环境下的特有头文件 **<arpa/inet.h>**。
  • SOCKET为Win环境下的特有数据类型,其原型为unsigned __int64,所以我们在Linux下,需要简单对SOCKET进行定义。
  • Linux中同样对INVALID_SOCKETSOCKET_ERROR也没有定义,所以我们参考Win中的定义,在Linux系统下对其定义。
    图1 (此图为Win环境下_socket_types.h头文件中的相关定义)

差异二

在Linux环境下不需要使用WSAStartupWSACleanup搭建网络环境,这是Win环境特有的。

  • 所以我们只需要加上判断即可,当检测到系统环境为Win时执行即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifdef _WIN32
//启动windows socket 2,x环境 windows特有
WORD ver = MAKEWORD(2,2);//WinSock库版本号
WSADATA dat;//网络结构体 储存WSAStartup函数调用后返回的Socket数据
if(0 != WSAStartup(ver,&dat))//正确初始化后返回0
{
return 0;
}
#endif

#ifdef _WIN32
//清除windows socket 环境
WSACleanup();
#endif

差异三

Linux环境与Win环境下,网络通信相关结构体 sockaddr_insockaddr 存在差异。
最明显的差异为存储IP的结构不太一样。

  • 所以我们这样更改即可:
    1
    2
    3
    4
    5
    #ifdef _WIN32
    _sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//想要连接的IP
    #else
    _sin.sin_addr.s_addr = inet_addr("127.0.0.1");//想要连接的IP
    #endif

差异四

Linux环境与Win环境下,关闭套接字的函数存在差异。
Win下为**closesocket(),Linux下则简单粗暴为close()**。

  • 所以我们这样更改即可:
    1
    2
    3
    4
    5
    6
    7
    #ifdef _WIN32
    //关闭socket
    closesocket(_mysocket);
    #else
    //关闭socket/LINUX
    close(_mysocket);
    #endif

差异五

Linux环境与Win环境下,服务器的accept连接函数参数存在差异。
Win下的最后一个参数为int型地址,Linux下则为socklen_t型地址。进行一次强制转换即可。

  • 所以我们这样更改即可:
    1
    2
    3
    4
    5
    #ifdef _WIN32	
    _temp_socket = accept(_mysocket,(sockaddr*)&_clientAddr,&_addr_len);//自身套接字 客户端结构体 结构体大小
    #else
    _temp_socket = accept(_mysocket,(sockaddr*)&_clientAddr,(socklen_t*)&_addr_len);//自身套接字 客户端结构体 结构体大小
    #endif
    差异六

Linux环境与Win环境下,fd_set结构体中的参数出现了变化,不再有储存socket数量的fd_count变量,所以我们需要对源码下select函数的第一个参数进行准确的数据传入。

select函数的第一个参数实际为 所有socket的最大值+1,所以我们新建一个变量,用于储存最大值。在每次对fdread集合进行导入时,找到socket的最大值,随后传入select函数即可。

  • 所以我们这样更改即可:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    SOCKET _maxSock = _mysocket;//最大socket

    for(int n=_clients.size()-1; n>=0; --n)//把连接的客户端 放入read集合
    {
    FD_SET(_clients[n],&_fdRead);
    if(_maxSock < _clients[n])
    {
    _maxSock = _clients[n];//找最大
    }
    }
    //select函数筛选select
    int _ret = select(_maxSock+1,&_fdRead,&_fdWrite,&_fdExcept,&_t);
    差异七

Linux环境与Win环境下,fd_set结构体中的参数出现了变化,不再有储存socket数量的fd_count变量,所以我们需要对源码下面关于遍历socket的逻辑进行改变。

首先遍历 _clients 数组中的所有socket,随后使用FD_ISSET函数判定其是否存在待处理事件,如果有,即可按逻辑进行处理。

  • 所以我们这样更改即可:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    for(int n=0; n<_clients.size(); ++n)//遍历所有socket
    {
    if(FD_ISSET(_clients[n],&_fdRead))//看一下是否在待处理事件列表中
    {
    if(-1 == _handle(_clients[n]))//处理请求 客户端退出的话
    {
    vector<SOCKET>::iterator iter = _clients.begin()+n;//找到退出客户端的地址
    if(iter != _clients.end())//如果是合理值
    {
    _clients.erase(iter);//移除
    }
    }
    }
    }

三、基于笔记三源码进行 跨平台化升级

1.客户端源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include<winSock2.h>
#include<windows.h>
#pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有
#else
#include<arpa/inet.h>//selcet
#include<unistd.h>//uni std
#include<string.h>

#define SOCKET int
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR (-1)
#endif

#include<bits/stdc++.h>
#include<thread>

using namespace std;

//枚举类型记录命令
enum cmd
{
CMD_LOGIN,//登录
CMD_LOGINRESULT,//登录结果
CMD_LOGOUT,//登出
CMD_LOGOUTRESULT,//登出结果
CMD_NEW_USER_JOIN,//新用户登入
CMD_ERROR//错误
};
//定义数据包头
struct DateHeader
{
short cmd;//命令
short date_length;//数据的长短
};
//包1 登录 传输账号与密码
struct Login : public DateHeader
{
Login()//初始化包头
{
this->cmd = CMD_LOGIN;
this->date_length = sizeof(Login);
}
char UserName[32];//用户名
char PassWord[32];//密码
};
//包2 登录结果 传输结果
struct LoginResult : public DateHeader
{
LoginResult()//初始化包头
{
this->cmd = CMD_LOGINRESULT;
this->date_length = sizeof(LoginResult);
}
int Result;
};
//包3 登出 传输用户名
struct Logout : public DateHeader
{
Logout()//初始化包头
{
this->cmd = CMD_LOGOUT;
this->date_length = sizeof(Logout);
}
char UserName[32];//用户名
};
//包4 登出结果 传输结果
struct LogoutResult : public DateHeader
{
LogoutResult()//初始化包头
{
this->cmd = CMD_LOGOUTRESULT;
this->date_length = sizeof(LogoutResult);
}
int Result;
};
//包5 新用户登入 传输通告
struct NewUserJoin : public DateHeader
{
NewUserJoin()//初始化包头
{
this->cmd = CMD_NEW_USER_JOIN;
this->date_length = sizeof(NewUserJoin);
}
char UserName[32];//用户名
};

int _handle(SOCKET _temp_socket)//处理数据
{
//接收客户端发送的数据
DateHeader _head = {};
int _buf_len = recv(_temp_socket,(char*)&_head,sizeof(DateHeader),0);
if(_buf_len<=0)
{
printf("与服务器断开连接,任务结束\n");
return -1;
}
printf("接收到包头,命令:%d,数据长度:%d\n",_head.cmd,_head.date_length);
switch(_head.cmd)
{
case CMD_LOGINRESULT://登录结果 接收登录包体
{
LoginResult _result;
recv(_temp_socket,(char*)&_result+sizeof(DateHeader),sizeof(LoginResult)-sizeof(DateHeader),0);
printf("登录结果:%d\n",_result.Result);
}
break;
case CMD_LOGOUTRESULT://登出结果 接收登出包体
{
LogoutResult _result;
recv(_temp_socket,(char*)&_result+sizeof(DateHeader),sizeof(LogoutResult)-sizeof(DateHeader),0);
printf("登录结果:%d\n",_result.Result);
}
break;
case CMD_NEW_USER_JOIN://新用户登录通知
{
NewUserJoin _result;
recv(_temp_socket,(char*)&_result+sizeof(DateHeader),sizeof(NewUserJoin)-sizeof(DateHeader),0);
printf("用户:%s已登录\n",_result.UserName);
}
}
return 0;
}

bool _run = true;//当前程序是否还在运行中
void _cmdThread(SOCKET _mysocket)//命令线程
{
while(_run)
{
//输入请求
char _msg[256] = {};
scanf("%s",_msg);
//处理请求
if(0 == strcmp(_msg,"exit"))
{
_run = false;
printf("程序退出\n");
break;
}
else if(0 == strcmp(_msg,"login"))
{
//发送
Login _login;
strcpy(_login.UserName,"hbxxy");
strcpy(_login.PassWord,"123456");
send(_mysocket,(const char*)&_login,sizeof(_login),0);
}
else if(0 == strcmp(_msg,"logout"))
{
//发送
Logout _logout;
strcpy(_logout.UserName,"hbxxy");
send(_mysocket,(const char*)&_logout,sizeof(_logout),0);
}
else
{
printf("不存在的命令\n");
}
}
}

int main()
{
printf("Welcome\n");

#ifdef _WIN32
//启动windows socket 2,x环境 windows特有
WORD ver = MAKEWORD(2,2);//WinSock库版本号
WSADATA dat;//网络结构体 储存WSAStartup函数调用后返回的Socket数据
if(0 != WSAStartup(ver,&dat))//正确初始化后返回0
{
return 0;
}
#endif

//建立一个socket
SOCKET _mysocket = socket(AF_INET,SOCK_STREAM,0);//IPV4 数据流类型 类型可以不用写
if(INVALID_SOCKET == _mysocket)//建立失败
{
printf("socket error");
return 0;
}

//连接服务器
sockaddr_in _sin = {};//sockaddr结构体
_sin.sin_family = AF_INET;//IPV4
_sin.sin_port = htons(8888);//想要连接的端口号

#ifdef _WIN32
_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//想要连接的IP
#else
_sin.sin_addr.s_addr = inet_addr("127.0.0.1");//想要连接的IP
#endif

if(SOCKET_ERROR == connect(_mysocket,(sockaddr*)&_sin,sizeof(sockaddr_in)))
{
cout<<"连接失败"<<endl;
#ifdef _WIN32
//关闭socket
closesocket(_mysocket);
#else
//关闭socket/LINUX
close(_mysocket);
#endif
}
else
{
cout<<"连接成功"<<endl;
}

//创建新线程
thread t1(_cmdThread,_mysocket);
t1.detach();//线程分离

while(_run)
{
fd_set _fdRead;//建立集合
FD_ZERO(&_fdRead);//清空集合
FD_SET(_mysocket,&_fdRead);//放入集合
timeval _t = {1,0};//select最大响应时间
//新建seclect
int _ret = select(_mysocket+1,&_fdRead,NULL,NULL,&_t);
if(_ret<0)
{
printf("seclect任务结束\n");
break;
}
if(FD_ISSET(_mysocket,&_fdRead))//获取是否有可读socket
{
FD_CLR(_mysocket,&_fdRead);//清理计数器
if(-1 == _handle(_mysocket))
{
printf("seclect任务结束\n");
break;
}
}
}

#ifdef _WIN32
//关闭socket
closesocket(_mysocket);
//清除windows socket 环境
WSACleanup();
#else
//关闭socket/LINUX
close(_mysocket);
#endif
return 0;
}

2.服务端源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include<winSock2.h>
#include<windows.h>
#pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有
#else
#include<arpa/inet.h>//selcet
#include<unistd.h>//uni std
#include<string.h>

#define SOCKET int
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR (-1)
#endif

#include<bits/stdc++.h>

using namespace std;

//枚举类型记录命令
enum cmd
{
CMD_LOGIN,//登录
CMD_LOGINRESULT,//登录结果
CMD_LOGOUT,//登出
CMD_LOGOUTRESULT,//登出结果
CMD_NEW_USER_JOIN,//新用户登入
CMD_ERROR//错误
};
//定义数据包头
struct DateHeader
{
short cmd;//命令
short date_length;//数据的长短
};
//包1 登录 传输账号与密码
struct Login : public DateHeader
{
Login()//初始化包头
{
this->cmd = CMD_LOGIN;
this->date_length = sizeof(Login);
}
char UserName[32];//用户名
char PassWord[32];//密码
};
//包2 登录结果 传输结果
struct LoginResult : public DateHeader
{
LoginResult()//初始化包头
{
this->cmd = CMD_LOGINRESULT;
this->date_length = sizeof(LoginResult);
}
int Result;
};
//包3 登出 传输用户名
struct Logout : public DateHeader
{
Logout()//初始化包头
{
this->cmd = CMD_LOGOUT;
this->date_length = sizeof(Logout);
}
char UserName[32];//用户名
};
//包4 登出结果 传输结果
struct LogoutResult : public DateHeader
{
LogoutResult()//初始化包头
{
this->cmd = CMD_LOGOUTRESULT;
this->date_length = sizeof(LogoutResult);
}
int Result;
};
//包5 新用户登入 传输通告
struct NewUserJoin : public DateHeader
{
NewUserJoin()//初始化包头
{
this->cmd = CMD_NEW_USER_JOIN;
this->date_length = sizeof(NewUserJoin);
}
char UserName[32];//用户名
};

vector<SOCKET> _clients;//储存客户端socket

int _handle(SOCKET _temp_socket)//处理数据
{
//接收客户端发送的数据
DateHeader _head = {};
int _buf_len = recv(_temp_socket,(char*)&_head,sizeof(DateHeader),0);
if(_buf_len<=0)
{
printf("客户端已退出\n");
return -1;
}
printf("接收到包头,命令:%d,数据长度:%d\n",_head.cmd,_head.date_length);
switch(_head.cmd)
{
case CMD_LOGIN://登录 接收登录包体
{
Login _login;
recv(_temp_socket,(char*)&_login+sizeof(DateHeader),sizeof(Login)-sizeof(DateHeader),0);
/*
进行判断操作
*/
printf("%s已登录\n密码:%s\n",_login.UserName,_login.PassWord);
LoginResult _result;
_result.Result = 1;
send(_temp_socket,(char*)&_result,sizeof(LoginResult),0);//发包体
}
break;
case CMD_LOGOUT://登出 接收登出包体
{
Logout _logout;
recv(_temp_socket,(char*)&_logout+sizeof(DateHeader),sizeof(Logout)-sizeof(DateHeader),0);
/*
进行判断操作
*/
printf("%s已登出\n",_logout.UserName);
LogoutResult _result;
_result.Result = 1;
send(_temp_socket,(char*)&_result,sizeof(LogoutResult),0);//发包体
}
break;
default://错误
{
_head.cmd = CMD_ERROR;
_head.date_length = 0;
send(_temp_socket,(char*)&_head,sizeof(DateHeader),0);//发包头
}
break;
}
return 0;
}

int main()
{
#ifdef _WIN32
//启动windows socket 2,x环境 windows特有
WORD ver = MAKEWORD(2,2);//WinSock库版本号
WSADATA dat;//网络结构体 储存WSAStartup函数调用后返回的Socket数据
if(0 != WSAStartup(ver,&dat))//正确初始化后返回0
{
return 0;
}
#endif

//建立一个socket
SOCKET _mysocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//IPV4 数据流类型 TCP类型
if(INVALID_SOCKET == _mysocket)//建立失败
{
return 0;
}

//绑定网络端口和IP地址
sockaddr_in _myaddr = {};//建立sockaddr结构体 sockaddr_in结构体方便填写 但是下面要进行类型转换
_myaddr.sin_family = AF_INET;//IPV4
_myaddr.sin_port = htons(8888);//端口 host to net unsigned short
#ifdef _WIN32
_myaddr.sin_addr.S_un.S_addr = INADDR_ANY;//inet_addr("127.0.0.1");
#else
_myaddr.sin_addr.s_addr = INADDR_ANY;//想要监听的ip
#endif
if(SOCKET_ERROR == bind(_mysocket,(sockaddr*)&_myaddr,sizeof(sockaddr_in)))//socket (强制转换)sockaddr结构体 结构体大小
{
cout<<"绑定不成功"<<endl;
}
else
{
//cout<<"绑定成功"<<endl;
}

//监听网络端口
if(SOCKET_ERROR == listen(_mysocket,5))//套接字 最大多少人连接
{
cout<<"监听失败"<<endl;
}
else
{
//cout<<"监听成功"<<endl;
}

while(true)
{
fd_set _fdRead;//建立集合
fd_set _fdWrite;
fd_set _fdExcept;
FD_ZERO(&_fdRead);//清空集合
FD_ZERO(&_fdWrite);
FD_ZERO(&_fdExcept);
FD_SET(_mysocket,&_fdRead);//放入集合
FD_SET(_mysocket,&_fdWrite);
FD_SET(_mysocket,&_fdExcept);
timeval _t = {2,0};//select最大响应时间
SOCKET _maxSock = _mysocket;//最大socket

for(int n=_clients.size()-1; n>=0; --n)//把连接的客户端 放入read集合
{
FD_SET(_clients[n],&_fdRead);
if(_maxSock < _clients[n])
{
_maxSock = _clients[n];//找最大
}
}
//select函数筛选select
int _ret = select(_maxSock+1,&_fdRead,&_fdWrite,&_fdExcept,&_t);
if(_ret<0)
{
printf("select任务结束\n");
break;
}
if(FD_ISSET(_mysocket,&_fdRead))//获取是否有新socket连接
{
FD_CLR(_mysocket,&_fdRead);//清理
//等待接收客户端连接
sockaddr_in _clientAddr = {};//新建sockadd结构体接收客户端数据
int _addr_len = sizeof(sockaddr_in);//获取sockadd结构体长度
SOCKET _temp_socket = INVALID_SOCKET;//声明客户端套接字
#ifdef _WIN32
_temp_socket = accept(_mysocket,(sockaddr*)&_clientAddr,&_addr_len);//自身套接字 客户端结构体 结构体大小
#else
_temp_socket = accept(_mysocket,(sockaddr*)&_clientAddr,(socklen_t*)&_addr_len);//自身套接字 客户端结构体 结构体大小
#endif
if(INVALID_SOCKET == _temp_socket)//接收失败
{
cout<<"接收到无效客户端Socket"<<endl;
}
else
{
cout<<"新客户端加入"<<endl;
printf("IP地址为:%s \n", inet_ntoa(_clientAddr.sin_addr));
//群发所有客户端 通知新用户登录
NewUserJoin _user_join;
strcpy(_user_join.UserName,inet_ntoa(_clientAddr.sin_addr));
for(int n=0;n<_clients.size();++n)
{
send(_clients[n],(const char*)&_user_join,sizeof(NewUserJoin),0);
}
//将新的客户端加入动态数组
_clients.push_back(_temp_socket);
}
}

for(int n=0; n<_clients.size(); ++n)//遍历所有socket
{
if(FD_ISSET(_clients[n],&_fdRead))//看一下是否在待处理事件列表中
{
if(-1 == _handle(_clients[n]))//处理请求 客户端退出的话
{
vector<SOCKET>::iterator iter = _clients.begin()+n;//找到退出客户端的地址
if(iter != _clients.end())//如果是合理值
{
_clients.erase(iter);//移除
}
}
}
}
printf("空闲时间处理其他业务\n");
}

#ifdef _WIN32
//关闭客户端socket
for(int n=0; n<_clients.size(); ++n)
{
closesocket(_clients[n]);
}
//关闭socket
closesocket(_mysocket);
//清除windows socket 环境
WSACleanup();
#else
//关闭客户端socket
for(int n=0; n<_clients.size(); ++n)
{
close(_clients[n]);
}
//关闭socket/LINUX
close(_mysocket);
#endif

printf("任务结束,程序已退出");

getchar();

return 0;
}

四、移植过程中遇到的一些小问题

1.关于IP的问题

  如果服务端在本机Windows环境下运行,客户端在VM虚拟机Linux环境下运行,则在Windows命令行上输入ipconfig命令。下面这一块数据下的IPv4地址即为客户端需要连接的IP。

1
2
3
4
5
6
7
以太网适配器 VMware Network Adapter VMnet8:

连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . :
IPv4 地址 . . . . . . . . . . . . :
子网掩码 . . . . . . . . . . . . :
默认网关. . . . . . . . . . . . . :

  如果服务端在VM虚拟机Linux环境下运行,客户端在本机Windows环境下运行,则在Linux命令行上输入ifconfig命令。显示出来的数据中网卡的IP即为客户端需要连接的IP。

2.关于端口的问题

  如果你的服务端运行正常,客户端运行正常,本机双开客户端和服务端也运行正常,但本机与虚拟机各开一个却连接不上时,可能是服务端的端口未开放导致的。

  • Windows环境下会主动提示,点击允许即可,如果还是不行就去网上搜。
  • Linux环境下相关命令如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    systemctl status firewalld 查看防火墙状态

    systemctl start firewalld 开启防火墙

    systemctl stop firewalld 关闭防火墙

    service firewalld start 开启防火墙

    查看对外开放的8888端口状态 yes/no
    firewall-cmd --query-port=8888/tcp

    打开8888端口
    firewall-cmd --add-port=8888/tcp --permanent

    重载端口
    firewall-cmd --reload

    移除指定的8888端口:
    firewall-cmd --permanent --remove-port=8888/tcp