网络编程学习记录
- 使用的语言为C/C++
- 源码支持的平台为:Windows
笔记二:网络数据报文的收发 ?点我跳转
笔记三:升级为select网络模型 ?点我跳转
笔记四:跨平台支持Windows、Linux系统 ?点我跳转
笔记五:源码的封装 ?点我跳转
笔记六:缓冲区溢出与粘包分包 ?点我跳转
笔记七:服务端多线程分离业务处理高负载 ?点我跳转
笔记三
- 网络编程学习记录
- 一、为何要使用select网络模型?
- 二、select系统及其相关
-
- ★ select相关使用总结与心得
- 三、升级为select网络模型的思路
-
- 1.服务端升级(select)
- 2.客户端升级(select+多线程)
- 四、代码及其详细注释
-
- 1.服务端代码
- 2.客户端代码
一、为何要使用select网络模型? ??通过前面的学习,已经实现了简单的网络报文收发。但是可以很明显的看出其中的缺点,那就是整个程序的运行是阻塞模式的。即服务端在与一个客户端进行socket连接时,只要连接不中断,那么就无法接收新的客户端的消息。而客户端在未输入命令时,是阻塞状态,也无法接收服务端发来的消息。
??在之前碰到这个问题时,我的想法是通过多线程来解决程序运行中的阻塞问题,但是在最近的学习中,我了解到可以使用select网络模型来方便快捷的解决小型网络程序运行中的阻塞问题。(I/O多路复用模型相关内容)
二、select系统及其相关 select函数如下:
WINSOCK_API_LINKAGE int WSAAPI select(
int nfds,//是指待监听集合里的范围 即待监听数量最大值+1
fd_set *readfds,//待监听的可读文件集合
fd_set *writefds,//待监听的可写文件集合
fd_set *exceptfds,//待监听的异常文件集合
const PTIMEVAL timeout);
//超时设置 传入NULL为阻塞模式 传入timeval结构体为非阻塞模式 返回值为满足条件的待监听socket数量和,如果出错返回-1,如果超时返回0。
通过上面select函数的参数可以发现存在两个特殊的结构体 fd_set 和 timeval,其相关内容如下:
typedef struct fd_set//可以存放多个socket
{u_int fd_count;
//记录放了多少个socket
SOCKET fd_array[FD_SETSIZE];
//socket数组
} fd_set;
struct timeval//时间结构体
{long tv_sec;
//秒
long tv_usec;
//毫秒
};
接下来为select的相关函数
void FD_SET(int fd, fd_set *set);
//将fd加入set集合
void FD_ZERO(fd_set *set);
//使set集合清零 不包含任何socket
void FD_CLR(int fd, fd_set *set);
//将fd从set集合中清除
intFD_ISSET(int fd, fd_set *set);
//测试fd是否在集合中 0是不在 1是在
★ select相关使用总结与心得 ??在一开始的select使用中,我以为向select函数中传入fd_set地址,select会把待处理事件的socket放在set集合中,但是发现并不是这样。
??经过网络上资料的查询以及我个人的测试,可以发现,用户首先需要把一份socket数组传入到此set中,select函数的作用是移除该set中没有待处理事件的socket,则剩下的socket都存在待处理事件(未决I/O操作)。这个过程可以说是一种“选择”的过程,select函数“选择”出需要操作的socket,这或许就是select(选择)的意思吧。
??在接下来的源码中,对于需要存储所有已连接socket的服务端,我使用动态数组vector进行socket的储存。在进行select筛选前,先把vector中的socket导入到set中,随后set中筛选剩下的即为有待处理事件的socket。
??如果服务端自己的socket提示有待处理事件,则说明有新的客户端尝试进行连接,此时进行accept操作即可。
??对于客户端的多线程问题,需要注意使用detach()方法使主线程与新线程分类,否则可能会出现主线程先结束的情况,导致程序出错。
??在线程中,我们可以引入一个bool变量,用来记录客户端是否仍在连接中,当输入exit命令退出客户端时,通过此bool变量使主线程停止,跳出循环。
三、升级为select网络模型的思路 1.服务端升级(select) 在之前,我们的思路是:
1.建立socket
2.绑定端口IP
3.监听端口
4.与客户端连接
while(true)
{ 5.接收数据
6.发送数据
}
7.关闭socket
??这就导致我们只能与一个客户端进行连接,随后便进入循环,只能接收这一个客户端的消息。且由于send与recv函数都是阻塞函数,所以程序也是阻塞模式的。
接下来,我们需要根据select网络模型,对服务端进行升级。
思路大致如下:
1.建立socket
2.绑定端口IP
3.监听端口
while(true)
{ 4.使用select函数获取存在待监听事件的socket
5.如果有新的连接则与新的客户端连接
6.如果有待监听事件,则对其进行处理(接受与发送)
}
7.关闭socket
??按如上思路,即可将程序升级为select网络模型。实现非阻塞模式,可以实现多客户端信息接收。对于select相关的细节与总结,请看上文中的总结。相关代码在下文。
2.客户端升级(select+多线程) 在之前,我们的思路是:
1.建立socket
2.连接服务器
while(true)
{ 3.发送数据
4.接收数据
}
5.关闭socket
??这就导致我们在与一个服务端连接后,无法被动的接收服务器端发来的消息。因为send与recv函数都是阻塞函数,程序也为阻塞模式。如果我们想要客户端能接收服务端发来的消息,那么就可以使用select模型。
【网络编程|C++网络编程学习(升级为select网络模型)】接下来,我们需要根据select网络模型,对客户端进行升级。
思路大致如下:
1.建立socket
2.连接服务器
while(true)
{ 3.使用select函数获取服务器端是否有待处理事件
4.如果有,就处理它(接收/发送)
}
5.关闭socket
??按如上思路,即可将程序升级为select网络模型。实现非阻塞模式,可以实现服务器端数据的被动接收。
但是,这样的程序结构也有很明显的缺点,因为scanf等数据接收函数也为阻塞函数,如果我们想要主动输入一些命令发送给服务端,就会阻塞程序运行。对此,我们可以引入多线程解决问题。
思路大致如下:
1.建立socket
2.连接服务器
3.建立新线程 用于发送命令
while(true)
{ 4.使用select函数获取服务器端是否有待处理事件
5.如果有,就处理它(接收/发送)
}
5.关闭socket新线程:
while(1)
{ 1.键入数据
2.发送数据
}
??按如上思路,即可将程序变得更加完善。可以被动接受数据且可以主动向服务端发送键入命令。对于select相关的细节与总结以及线程方面的注意事项,请看上文中的总结。相关代码在下文。
四、代码及其详细注释 1.服务端代码
#define WIN32_LEAN_AND_MEAN#include
#include
#include#pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有 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> _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()
{ //启动windows socket 2,x环境 windows特有
WORD ver = MAKEWORD(2,2);
//WinSock库版本号
WSADATA dat;
//网络结构体 储存WSAStartup函数调用后返回的Socket数据
if(0 != WSAStartup(ver,&dat))//正确初始化后返回0
{return 0;
}
//建立一个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
_myaddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
//网络地址 INADDR_ANY监听所有网卡的端口
if(SOCKET_ERROR == bind(_mysocket,(sockaddr*)&_myaddr,sizeof(sockaddr_in)))//socket (强制转换)sockaddr结构体 结构体大小
{cout<<"绑定不成功"<=0;
--n)//把连接的客户端 放入read集合
{FD_SET(_clients[n],&_fdRead);
}
//select函数筛选select
int _ret = select(_mysocket+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;
//声明客户端套接字 _temp_socket = accept(_mysocket,(sockaddr*)&_clientAddr,&_addr_len);
//自身套接字 客户端结构体 结构体大小
if(INVALID_SOCKET == _temp_socket)//接收失败
{cout<<"接收到无效客户端Socket"<::iterator iter = find(_clients.begin(),_clients.end(),_fdRead.fd_array[n]);
if(iter != _clients.end())//如果找到了的话 就在动态数组里删除掉
{_clients.erase(iter);
}
}
}
printf("空闲时间处理其他业务\n");
} //关闭客户端socket
for(int n=0;
n<_clients.size();
++n)
{closesocket(_clients[n]);
} //关闭socket
closesocket(_mysocket);
//清除windows socket 环境
WSACleanup();
printf("任务结束,程序已退出");
getchar();
return 0;
}
2.客户端代码
#define WIN32_LEAN_AND_MEAN#include
#include
#include
#include#pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有 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(true)
{//输入请求
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,"河边小咸鱼");
strcpy(_login.PassWord,"123456");
send(_mysocket,(const char*)&_login,sizeof(_login),0);
//这里就不用接收了 由select用来检测接收
}
else if(0 == strcmp(_msg,"logout"))
{//发送
Logout _logout;
strcpy(_logout.UserName,"河边小咸鱼");
send(_mysocket,(const char*)&_logout,sizeof(_logout),0);
//这里就不用接收了 由select用来检测接收
}
else
{printf("不存在的命令\n");
}
}
}int main()
{ //启动windows socket 2,x环境 windows特有
WORD ver = MAKEWORD(2,2);
//WinSock库版本号
WSADATA dat;
//网络结构体 储存WSAStartup函数调用后返回的Socket数据
if(0 != WSAStartup(ver,&dat))//正确初始化后返回0
{return 0;
}
//建立一个socket
SOCKET _mysocket = socket(AF_INET,SOCK_STREAM,0);
//IPV4 数据流类型 类型可以不用写
if(INVALID_SOCKET == _mysocket)//建立失败
{return 0;
} //连接服务器
sockaddr_in _sin = {
};
//sockaddr结构体
_sin.sin_family = AF_INET;
//IPV4
_sin.sin_port = htons(8888);
//想要连接的端口号
_sin.sin_addr.S_un.S_addr =inet_addr("127.0.0.1");
//想要连接的IP
if(SOCKET_ERROR == connect(_mysocket,(sockaddr*)&_sin,sizeof(sockaddr_in)))
{cout<<"连接失败"<
推荐阅读
- 游戏|UnityShader 表面着色器简单例程集合
- python|python c dll_Python使用Ctypes与C/C++ DLL文件通信过程介绍及实例分析
- 网络编程|java nio ByteBuffer的使用
- 数据结构|课程设计(飞机订票系统) 超全
- 网络编程|TCP/UDP小记
- 程序员|编程详谈(程序员真的是高薪吗(那些你不了解的内幕))
- C/C++预处理浅析使用形式
- C|Rust : 如何将C字符串转换为Rust字符串并通过FFI返回()
- 数据结构|【数据结构】【王道】【线性表】无头结点单链表的实现及基本操作(可直接运行)