笔记一:建立基础TCP服务端/客户端 点我跳转
笔记二:网络数据报文的收发 点我跳转
笔记三:升级为select网络模型 点我跳转
笔记四:跨平台支持Windows、Linux系统 点我跳转
笔记五:源码的封装 点我跳转
笔记六:缓冲区溢出与粘包分包 点我跳转
笔记七:服务端多线程分离业务处理高负载 点我跳转
首先,我是想在网络编程学习渐入佳境后,自己尝试做一个网络方面的项目,其中就必须用到服务器。Linux服务器相比Windows服务器更加稳定且高效,所以对于我来说,学会如何编写出可以在Linux系统下运行的网络程序是必不可少的。
其次,就目前来说,企业中的高性能网络编程都是基于Linux的,学会跨平台的网络编程技能,可以在未来就业方面等有很大的好处。
由此,我决定在网络编程学习的第四小阶段,学习如何进行跨平台的网络编程。
差异一
在Linux环境下,程序的头文件与定义与Win环境下存在差异。
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include
#include
#pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有
#else
#include //selcet
#include //uni std
#include
#define SOCKET int
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR (-1)
#endif
这是更改后的程序部分。
可以看出:
差异二
在Linux环境下不需要使用WSAStartup与WSACleanup搭建网络环境,这是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_in和sockaddr 存在差异。
最明显的差异为存储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);//移除
}
}
}
}
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include
#include
#pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有
#else
#include //selcet
#include //uni std
#include
#define SOCKET int
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR (-1)
#endif
#include
#include
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;
}
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include
#include
#pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有
#else
#include //selcet
#include //uni std
#include
#define SOCKET int
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR (-1)
#endif
#include
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<<"绑定成功"<
}
//监听网络端口
if(SOCKET_ERROR == listen(_mysocket,5))//套接字 最大多少人连接
{
cout<<"监听失败"<<endl;
}
else
{
//cout<<"监听成功"<
}
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;
}
如果服务端在本机Windows环境下运行,客户端在VM虚拟机Linux环境下运行,则在Windows命令行上输入ipconfig命令。下面这一块数据下的IPv4地址即为客户端需要连接的IP。
以太网适配器 VMware Network Adapter VMnet8:
连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . :
IPv4 地址 . . . . . . . . . . . . :
子网掩码 . . . . . . . . . . . . :
默认网关. . . . . . . . . . . . . :
如果服务端在VM虚拟机Linux环境下运行,客户端在本机Windows环境下运行,则在Linux命令行上输入ifconfig命令。显示出来的数据中网卡的IP即为客户端需要连接的IP。
如果你的服务端运行正常,客户端运行正常,本机双开客户端和服务端也运行正常,但本机与虚拟机各开一个却连接不上时,可能是服务端的端口未开放导致的。
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