结构图: 使用到的技术:
- socket编程
- select轮循
- TCP协议
- 消息的分包
TCP四层协议
应用层 | 为用户的应用提供网络服务 |
---|
传输层 | 定义传输的协议,ip,端口号 | 网络层 | 为不同地理位置的网络主机提供连接和路径选择 | 数据链路层 | 让格式化数据以帧为单位传输,差错校验,物理寻址 |
- 什么是socket:socket是对底层网络通信的一层抽象,让程序员可以像文件那也操作网络上发送和接收的数据
- 通信地址:
- ip:是网络层用来路由和通信的标识符
- 端口:传输层管理
- 协议:ipv4 ipv6
- socket类型:
- SOCK_STREAM 流式协议,面向连接的稳定通信,底层式TCP
- SOCK_DGRAM 报文式协议,面向无连接底层是UDP协议,需要上层协议保证可靠性
- SOCK_RAW 更加灵活的数据控制,可以指定IP头部
术语表:
名称 | 含义 |
---|
socket | 创建一个通信管道 | bind | 把一个地址三元组绑定到socket上,ip,端口,协议 | listen | 监听,准备接收某个socket的数据 | accept | 等待连接到达 | connect | 主动建立连接 | send | 发送数据 | receive | 接收数据 | close | 关闭连接 |
函数的声明如下:
int select(int nfds, fd_set* readset, fd_set* writeset, fe_set* exceptset, struct timeval* timeout);
参数:
nfds 需要检查的文件描述字个数 readset 用来检查可读性的一组文件描述字。 writeset 用来检查可写性的一组文件描述字。 exceptset 用来检查是否有异常条件出现的文件描述字。(注:错误不包括在异常条件之内) timeout 超时,填NULL为阻塞,填0为非阻塞,其他为一段超时时间
select轮循的原理: select循环遍历它所监测的fd_set内的所有文件描述符所对应的驱动程序的poll函数。 fd_set结构体: FD_SET(int fd, fd_set *fdset); //将fd加入set集合 FD_CLR(int fd, fd_set *fdset); //将fd从set集合中清除 FD_ISSET(int fd, fd_set *fdset); //检测fd是否在set集合中,不在则返回0 FD_ZERO(fd_set *fdset); //将set清零使集合中不含任何fd 然后是我写的服务器的内核: sever.h
#include <WinSock2.h>
#include <map>
namespace XS
{
class Client;
class Sever
{
private:
SOCKET _seversocket;
std::map<SOCKET, Client*> _clients;
public:
bool Listen(const char* Ip, int Port);
void Update();
void Close();
private:
void DoAccept();
public:
virtual void OnAccept(Client* client) = 0;
virtual void OnNetMsg(Client* client) = 0;
virtual void Disconncet(Client* client) = 0;
};
}
sever.cpp
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include "Sever.h"
#include <iostream>
#include "Client.h"
#include <vector>
namespace XS
{
bool Sever::Listen(const char* Ip, int Port)
{
SOCKET seversocket = socket(AF_INET, SOCK_STREAM, 0);
if (seversocket == INVALID_SOCKET)
{
printf("创建套接字失败 %d\n", GetLastError());
return false;
}
SOCKADDR_IN severAddr;
severAddr.sin_family = AF_INET;
severAddr.sin_port = htons(Port);
severAddr.sin_addr.S_un.S_addr = inet_addr(Ip);
if (SOCKET_ERROR == bind(seversocket, (SOCKADDR*)&severAddr, sizeof(SOCKADDR_IN)))
{
printf("bind Error %d\n", GetLastError());
return false;
}
printf("Bind OK IP:[%s] Port:[%d]\n", Ip, Port);
if (SOCKET_ERROR == listen(seversocket, 5))
{
printf("Listen Error %d\n", GetLastError());
return false;
}
_seversocket = seversocket;
return true;
}
void Sever::Update()
{
FD_SET fdReads;
FD_ZERO(&fdReads);
FD_SET(_seversocket, &fdReads);
auto begin = _clients.begin();
auto end = _clients.end();
for (; begin != end; ++begin)
{
FD_SET(begin->first, &fdReads);
}
int nRent = select(0, &fdReads, nullptr, nullptr, 0);
if (nRent <= 0)
{
return;
}
if (FD_ISSET(_seversocket, &fdReads))
{
DoAccept();
}
begin = _clients.begin();
end = _clients.end();
std::vector<std::map<SOCKET, Client*>::iterator> _close;
for (; begin != end; ++begin)
{
if (FD_ISSET(begin->first, &fdReads))
{
Client* client = begin->second;
if (!client->DoRecv())
{
_close.push_back(begin);
}
}
}
for (int i = _close.size() - 1; i >= 0; i--)
{
_close[i]->second->DoClose();
delete _close[i]->second;
_clients.erase(_close[i]);
}
}
void Sever::DoAccept()
{
SOCKET clientsocket;
SOCKADDR_IN clientAddr;
int clientAddrLen = sizeof(SOCKADDR_IN);
clientsocket = accept(_seversocket, (SOCKADDR*)&clientAddr, &clientAddrLen);
if (clientsocket == INVALID_SOCKET)
{
printf("Accept Error %d\n", GetLastError());
return;
}
printf("accept OK SOCKET:[%d] Ip:[%s] Port:[%d]\n",
clientsocket, inet_ntoa(clientAddr.sin_addr),
ntohs(clientAddr.sin_port));
Client* pclient = new Client(clientsocket,
inet_ntoa(clientAddr.sin_addr),
ntohs(clientAddr.sin_port), this);
_clients.insert(std::pair<SOCKET, Client*>(clientsocket, pclient));
OnAccept(pclient);
}
}
其他的内容放入到我的资源中,需要源码的请自提! 下面献上运行结果: 用户注册: 用户登录: 其他的就不演示了,这里说一下房间的设计思路! 房间最好是设置一个房间管理器,有房间管理器管理房间和用户,用户下线是修改用户的状态,实际还是存在于用户容器中的,再次上线也只需要修改状态不用再做删除和增加的操作。也就是逻辑删除
|