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

网络编程学习记录

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

笔记一:建立基础TCP服务端/客户端  点我跳转
笔记二:网络数据报文的收发  点我跳转
笔记三:升级为select网络模型  点我跳转
笔记四:跨平台支持Windows、Linux系统  点我跳转
笔记五:源码的封装  点我跳转
笔记六:缓冲区溢出与粘包分包  点我跳转
笔记七:服务端多线程分离业务处理高负载  点我跳转


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

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

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

差异一

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

#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时执行即可:
	#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的结构不太一样。

  • 所以我们这样更改即可:
	#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()

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

差异五

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

  • 所以我们这样更改即可:
	#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函数即可。

  • 所以我们这样更改即可:
	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函数判定其是否存在待处理事件,如果有,即可按逻辑进行处理。

  • 所以我们这样更改即可:
	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.客户端源码

#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.服务端源码

#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。

以太网适配器 VMware Network Adapter VMnet8:

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

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

2.关于端口的问题

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

  • Windows环境下会主动提示,点击允许即可,如果还是不行就去网上搜。
  • Linux环境下相关命令如下:
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

猜你喜欢

转载自blog.csdn.net/qq_45698148/article/details/113095420