以下是select模型实现的一个简易TCP服务端和客户端,客户端添加了一个命令输入线程
server
#ifdef _WIN32
//尽量避免早期的宏的引入
#define WIN32_LEAN_AND_MEAN
//Windows.h里面的宏和WinSock2里面的宏有重复
//Windows环境下开发
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <Windows.h>
//windows环境下开发网络编程需要引入的socket头文件
#include <WinSock2.h>
//windows环境下进行引入动态链接库 WSAStartup
//在其他系统平台下不能使用 可以将ws2_32.lib 配置到工程 属性 链接器里面
//#pragma comment(lib, "ws2_32.lib")
#else
#include<unistd.h>
#include<arpa/inet.h>
#include<string.h>
#define SOCKET int
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR (-1)
#endif
#include<stdio.h>
#include<vector>
//内存对齐
struct DataPackage
{
//long 在64位中是64字节 在32位只有4字节
int age;
char name[32];
};
struct DataHeader
{
short dataLength;
short cmd;
};
enum CMD
{
CMD_LOGIN,
CMD_LOGINOUT,
CMD_LOGIN_RESULT,
CMD_LOGOUT_RESULT,
CMD_NEW_USER_JOIN,
CMD_ERROR
};
struct Login : public DataHeader
{
Login()
{
dataLength = sizeof(Login);
cmd = CMD_LOGIN;
}
char userName[32];
char passWord[32];
};
struct LoginOut : public DataHeader
{
LoginOut()
{
dataLength = sizeof(LoginOut);
cmd = CMD_LOGINOUT;
}
char userName[32];
};
struct LoginResult : public DataHeader
{
LoginResult()
{
dataLength = sizeof(LoginResult);
cmd = CMD_LOGIN_RESULT;
result = 1;
}
int result;
};
struct LogoutResult: public DataHeader
{
LogoutResult()
{
dataLength = sizeof(LoginResult);
cmd = CMD_LOGOUT_RESULT;
result = 0;
}
int result;
};
struct NewUserJoin : public DataHeader
{
NewUserJoin()
{
dataLength = sizeof(NewUserJoin);
cmd = CMD_NEW_USER_JOIN;
sock_id = 0;
}
int sock_id;
};
std::vector<SOCKET> g_clients;
int process(SOCKET _cSock)
{
//缓冲区,把消息先放到缓冲区里面
char szRecv[1024] = {};
//5接收客户端数据
int nLen = (int)recv(_cSock, (char*)&szRecv, sizeof(DataHeader), 0);
DataHeader* header = (DataHeader*)szRecv;
if (nLen <= 0) {
printf("客户端<Socket=%d>已退出,任务结束。\n", _cSock);
return -1;
}
switch (header->cmd) {
case CMD_LOGIN:
{
recv(_cSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
//取到消息 把消息对应到消息体里面
Login* login = (Login*)szRecv;
printf("收到客户端<Socket=%d>请求:cmd_login 数据长度:%d, userName=%s, passWd = %s \n", _cSock , login->dataLength, login->userName, login->passWord);
//忽略判断用户名密码是否正确
LoginResult loginRet;
send(_cSock, (char*)&loginRet, sizeof(LoginResult), 0);
}
break;
case CMD_LOGINOUT:
{
recv(_cSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
LoginOut* logout = (LoginOut*)szRecv;
printf("收到客户端<Socket=%d>请求:cmd_logout 数据长度:%d, userName=%s \n", _cSock , logout->dataLength, logout->userName);
//忽略判断用户名密码是否正确
LogoutResult logoutRet;
send(_cSock, (char*)&logoutRet, sizeof(LogoutResult), 0);
}
break;
default:
header->cmd = CMD_ERROR;
header->dataLength = 0;
send(_cSock, (char*)&header, sizeof(header), 0);
break;
}
return 0;
}
int main()
{
#ifdef _WIN32
//创建版本号
WORD ver = MAKEWORD(2, 2);
WSADATA dat;
WSAStartup(ver, &dat);
#endif
//1 建立一个socket
//2 bind 绑定用于接收客户端连接的网络端口
//3 listen 监听网络端口
//4 accept 阻塞等待接受客户端连接
//5 send 向客户端发送一条数据
//6 关闭套接字 close socket
//1 建立一个socket
//创建一个AF_INET的套接字,代表ipv4
//SOCK_STREAM 面向数据流的 还可以选择面向蓝牙的等等
//IPPROTO_TCP面向tcp的 也可以选择udp等
SOCKET _sock = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP);
//2 bind 绑定用于接收客户端连接的网络端口
sockaddr_in _sin = {};
_sin.sin_family = AF_INET;
_sin.sin_port = htons(4567); //host to net unsigned short 字节序转换
//设置绑定到那个IP地址上 一台机器有很多地址
#ifdef _WIN32
_sin.sin_addr.S_un.S_addr = INADDR_ANY; //inet_addr("127.0.0.1");
#else
_sin.sin_addr.s_addr = INADDR_ANY;
#endif
if (SOCKET_ERROR == bind(_sock, (sockaddr*)&_sin, sizeof(_sin)))
{
printf("ERROR,绑定网络端口失败\n");
}
else {
printf("绑定网络端口成功\n");
}
//3 listen 监听网络端口
//backlog 5 backlog告诉内核使用多大数值创建等待队列,等待请求 一般小于30以内
if (SOCKET_ERROR == listen(_sock, 5))
{
printf("ERROR,监听网络端口失败\n");
}
else {
printf("监听网络端口成功\n");
}
while (true)
{
fd_set fdRead;
fd_set fdWrite;
fd_set fdExp;
FD_ZERO(&fdRead);
FD_ZERO(&fdWrite);
FD_ZERO(&fdExp);
FD_SET(_sock, &fdExp);
FD_SET(_sock, &fdWrite);
FD_SET(_sock, &fdRead);
SOCKET maxSock = _sock;
for (int n = (int)g_clients.size() - 1; n >= 0; n--)
{
FD_SET(g_clients[n], &fdRead);
if(maxSock < g_clients[n])
{
maxSock = g_clients[n];
}
}
//select最后一个参数 设置时间t,将select变为非阻塞 查询等待时间,到了t时间没有请求,返回
//1s 是最大的阻塞时间值,不一定等1s
timeval t = {1,0};
//伯克利 socket
//nfds 第一个参数,是指fd_set集合中所有描述符(socket)范围,socket最大的值加1 在windows下面不产生意义,可以写0,在linux下面代表最大连接数加1
int ret = select(maxSock+1, &fdRead, &fdWrite, &fdExp, &t);
if (ret < 0)
{
printf("select任务结束。 \n");
break;
}
//判断当前socket是否在当前fdSet集合中
if (FD_ISSET(_sock, &fdRead))
{
FD_CLR(_sock, &fdRead);
//4 accept 阻塞等待接受客户端连接
//客户端地址
sockaddr_in clientAddr = {};
int nAddrLen = (int)sizeof(clientAddr);
SOCKET _cSock = INVALID_SOCKET;
//循环accept多次
#ifdef _WIN32
_cSock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);
#else
_cSock = accept(_sock, (sockaddr*)&clientAddr, (socklen_t *)&nAddrLen);
#endif
if (_cSock == INVALID_SOCKET)
{
printf("ERROR,接收到无效客户端连接\n");
}
else
{
for (int n = (int)g_clients.size() - 1; n >= 0; n--)
{
NewUserJoin userJoin;
send(g_clients[n], (const char*)&userJoin, sizeof(NewUserJoin), 0);
}
g_clients.push_back(_cSock);
printf("新客户端加入:ip = %s \n", inet_ntoa(clientAddr.sin_addr));
}
}
// fdRead结构体在windows系统有fd_count
// for (size_t n = 0; n < fdRead.fd_count; n++)
// {
// if (-1 == process(fdRead.fd_array[n]))
// {
// auto iter = find(g_clients.begin(), g_clients.end(), fdRead.fd_array[n]);
// if (iter != g_clients.end())
// {
// g_clients.erase(iter);
// }
// }
// }
for(int n = (int)g_clients.size() - 1; n >= 0; n--)
{
if(FD_ISSET(g_clients[n], &fdRead))
{
if(-1 == process(g_clients[n]))
{
auto iter = g_clients.begin()+n;
if(iter != g_clients.end())
{
g_clients.erase(iter);
}
}
}
}
//printf("空闲处理其他业务\n");
}
#ifdef _WIN32
//8 关闭套接字 close socket
// for (size_t n = g_clients.size() - 1; n >= 0; n--)
for (int n = (int)g_clients.size() - 1; n >= 0; n--)
{
closesocket(g_clients[n]);
}
closesocket(_sock);
// 清楚windows socket环境
WSACleanup();
#else
//size_t 是无符号的,大于等于0是永远true的
for (int n = (int)g_clients.size() - 1; n >= 0; n--)
{
close(g_clients[n]);
}
close(_sock);
#endif
getchar();
return 0;
}
EasyTcpClient.hpp
#ifndef _EasyTcpClient_hpp_
#define _EasyTcpClient_hpp_
//Windows.h里面的宏和WinSock2里面的宏有重复
//Windows环境下开发
#ifdef _WIN32
//尽量避免早期的宏的引入
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
//windows环境下开发网络编程需要引入的socket头文件
#include <WinSock2.h>
//windows环境下进行引入动态链接库 WSAStartup
//在其他系统平台下不能使用 可以将ws2_32.lib 配置到工程 属性 链接器里面
#pragma comment(lib, "ws2_32.lib")
#else
#include<unistd.h>
#include<arpa/inet.h>
#include<string.h>
#define SOCKET int
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR (-1)
#endif
#include<stdio.h>
#include "msg.hpp"
class EasyTcpClient
{
SOCKET _sock;
public:
EasyTcpClient()
{
_sock = INVALID_SOCKET;
}
// 虚析构函数
virtual ~EasyTcpClient()
{
Close();
}
//初始化socket
void InitSocket()
{
//启动Win sock 2.x环境
#ifdef _WIN32
//创建版本号
WORD ver = MAKEWORD(2, 2);
WSADATA dat;
WSAStartup(ver, &dat);
#endif
//1 建立socket
if (_sock != INVALID_SOCKET)
{
//关闭之前连接
printf("关闭旧连接<socket=%d>.... \n", _sock);
Close();
}
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock == INVALID_SOCKET) {
printf("error, build socket faild \n");
}
else {
printf("build success \n");
}
}
//连接服务器
int Connect(const char* ip, unsigned short port)
{
if (_sock == INVALID_SOCKET)
{
InitSocket();
}
//2 连接服务器connect
sockaddr_in _sin = {};
_sin.sin_family = AF_INET;
_sin.sin_port = htons(port);
#ifdef _WIN32
_sin.sin_addr.S_un.S_addr = inet_addr(ip);
#else
_sin.sin_addr.s_addr = inet_addr(ip);
#endif
int ret = connect(_sock, (sockaddr*)&_sin, sizeof(sockaddr_in));
if (ret == SOCKET_ERROR) {
printf("<socket=%d>错误, 连接服务器<%s:%d>失败...... \n", _sock, ip, port);
}
else {
printf("<socket=%d>连接服务器<%s:%d>成功....... \n", _sock, ip, port);
}
return ret;
}
//4 关闭套接字closesocket
void Close()
{
if (_sock != INVALID_SOCKET)
{
//清除Win sock 2.x环境
#ifdef _WIN32
closesocket(_sock);
//清除windows socket环境
WSACleanup();
#else
close(_sock);
#endif
_sock = INVALID_SOCKET;
}
}
//处理网络消息
bool OnRun()
{
if (isRun())
{
fd_set fdReader;
FD_ZERO(&fdReader);
FD_SET(_sock, &fdReader);
timeval t = { 1, 0 };
int ret = select(_sock, &fdReader, 0, 0, &t);
if (ret < 0)
{
printf("<socket=%d>select 任务结束1\n", _sock);
return false;
}
if (FD_ISSET(_sock, &fdReader))
{
FD_CLR(_sock, &fdReader);
if (-1 == RecvData(_sock))
{
printf("<socket=%d>select任务结束2\n", _sock);
return false;
}
}
return true;
}
return false;
}
//是否工作中
bool isRun()
{
return _sock != INVALID_SOCKET;
}
//接收数据 粘包 拆包
int RecvData(SOCKET _cSock)
{
//缓冲区,把消息先放到缓冲区里面
char szRecv[1024] = {};
//5接收客户端数据
int nLen = recv(_cSock, (char*)&szRecv, sizeof(DataHeader), 0);
DataHeader* header = (DataHeader*)szRecv;
if (nLen <= 0)
{
printf("<socket=%d>与服务器断开连接,任务结束。\n", _cSock);
return -1;
}
recv(_cSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
OnNetMsg(header);
return 0;
}
//响应网络消息
void OnNetMsg(DataHeader* header)
{
switch (header->cmd) {
case CMD_LOGIN_RESULT:
{
//基类转换成子类
LoginResult* login = (LoginResult*)header;
printf("<socket=%d>收到服务端消息:CMD_LOGIN_RESULT 数据长度:%d \n", _sock, login->dataLength);
}
break;
case CMD_LOGOUT_RESULT:
{
LogoutResult* logout = (LogoutResult*)header;
printf("<socket=%d>收到服务端消息:CMD_LOGOUT_RESULT 数据长度:%d \n", _sock, logout->dataLength);
}
break;
case CMD_NEW_USER_JOIN:
{
NewUserJoin* user_join = (NewUserJoin*)header;
printf("<socket=%d>收到服务端消息:CMD_NEW_USER_JOIN 数据长度:%d \n", _sock, user_join->dataLength);
}
break;
}
}
//发送数据
int SendData(DataHeader* header)
{
if (isRun() && header)
{
return send(_sock, (const char*)header, header->dataLength, 0);
}
return SOCKET_ERROR;
}
private:
};
#endif
msg.hpp
//防止多次引入 msg.hpp头文件
#ifndef _MSG_HPP_
#define _MSG_HPP_
//内存对齐
struct DataPackage
{
//long 在64位中是64字节 在32位只有4字节
int age;
char name[32];
};
struct DataHeader
{
short dataLength;
short cmd;
};
enum CMD
{
CMD_LOGIN,
CMD_LOGINOUT,
CMD_LOGIN_RESULT,
CMD_LOGOUT_RESULT,
CMD_NEW_USER_JOIN,
CMD_ERROR
};
struct Login : public DataHeader
{
Login()
{
dataLength = sizeof(Login);
cmd = CMD_LOGIN;
}
char userName[32];
char passWord[32];
};
struct LoginOut : public DataHeader
{
LoginOut()
{
dataLength = sizeof(LoginOut);
cmd = CMD_LOGINOUT;
}
char userName[32];
};
struct LoginResult : public DataHeader
{
LoginResult()
{
dataLength = sizeof(LoginResult);
cmd = CMD_LOGIN_RESULT;
result = 1;
}
int result;
};
struct LogoutResult : public DataHeader
{
LogoutResult()
{
dataLength = sizeof(LoginResult);
cmd = CMD_LOGOUT_RESULT;
result = 0;
}
int result;
};
struct NewUserJoin : public DataHeader
{
NewUserJoin()
{
dataLength = sizeof(NewUserJoin);
cmd = CMD_NEW_USER_JOIN;
sock_id = 0;
}
int sock_id;
};
#endif // !_MSG_HPP_
client.cpp
#include "EasyTcpClient.hpp"
//引入c++标准线程库 c++11正式加入标准库 还有第三方线程库pthread
#include<thread>
//传引用写法 在其他平台下会有错误 使用指针
void cmdThread(EasyTcpClient* client)
{
while (true)
{
char cmdBuff[256] = {};
scanf_s("%s", cmdBuff);
if (0 == strcmp(cmdBuff, "exit"))
{
client->Close();
printf("退出cmdThread线程 \n");
break;
}
else if (0 == strcmp(cmdBuff, "login"))
{
Login login;
strcpy_s(login.userName, "lanzhibo");
strcpy_s(login.passWord, "123");
client->SendData(&login);
}
else if (0 == strcmp(cmdBuff, "logout"))
{
LoginOut logout;
strcpy_s(logout.userName, "lanzhibo");
client->SendData(&logout);
}
else
{
printf("不支持的命令\n");
}
}
}
int main()
{
EasyTcpClient client;
client.InitSocket();
client.Connect("127.0.0.1", 8888);
//线程 thread scanf是阻塞的 使用线程异步获取输入
std::thread t1(cmdThread, &client);
//与主线程分离
t1.detach();
while (client.isRun()) {
client.OnRun();
}
client.Close();
getchar();
return 0;
}