5.2---将服务端代码进行跨平台移植(Windows、Unix)

一、跨平台移植概述
前面的文章我们的代码都是在Windows下进行编译运行的,现在我们需要将代码移动到Linux和Mac OS操作系统上进行运行
因为Linux和Mac OS底层都是使用Unix内核,因此将代码修改之后,在Linux和Mac OS上面都可以进行编译并运行,不需要单独设计两份
二、代码修订
代码修订1
头文件修订:Windows与Unix下套接字使用的头文件不同,因此需要修订
一些常量定义:Windows下有SOCKET、、INVALID_SOCKET、SOCKET_ERROR等宏的定义,但是Unix没有。通过查看WinSock2.h头文件源码,可以看到有如下几张图片的定义,因此我们当在Unix系统中运行时,我们也手动在程序中进行了宏定义
在这里插入图片描述
在这里插入图片描述

//修改代码1如下
#ifdef _WIN32
    #define WIN32_LEAN_AND_MEAN
    #define _WINSOCK_DEPRECATED_NO_WARNINGS //for inet_pton()
    #define _CRT_SECURE_NO_WARNINGS
    #include <windows.h>
    #include <WinSock2.h>
    #pragma comment(lib, "ws2_32.lib")
#else
	#include <unistd.h>
	#include <sys/socket.h>
	#include <sys/types.h>
	#include <arpa/inet.h>
	#include <netinet/in.h>
	#include <sys/select.h>        
 
    //在Unix下没有这些宏,为了兼容,自己定义
    #define SOCKET int
    #define INVALID_SOCKET  (SOCKET)(~0)
    #define SOCKET_ERROR            (-1)
#endif
//修改代码2如下
//在Windows下需要开启套接字环境
#ifdef _WIN32
    WORD ver = MAKEWORD(2, 2);
    WSADATA dat;
    WSAStartup(ver, &dat);
#endif
 
//...省略中间代码
 
#ifdef _WIN32
    //将所有的客户端套接字关闭
    for (int n = (int)g_clients.size() - 1; n >= 0; --n)
    {
        closesocket(g_clients[n]);
    }
 
    //关闭服务端套接字
    closesocket(_sock);
    WSACleanup();
#else
    for (int n = (int)g_clients.size() - 1; n >= 0; --n)
    {
        close(g_clients[n]);
    }
    close(_sock);
#endif

代码修订3
另外,Windows与Unix下struct sockaddr_in的定义也不同,因此还需要进行如下的修改
本文会在Windows和Ubuntu下都运行一次服务器,因此下面的IP地址需要随时改动

//初始化服务端地址
struct sockaddr_in _sin = {};
#ifdef _WIN32
    _sin.sin_addr.S_un.S_addr = inet_addr("192.168.0.106");
//_sin.sin_addr.S_un.S_addr = INADDR_ANY;
#else
    _sin.sin_addr.s_addr = inet_addr("192.168.0.106");
#endif
_sin.sin_family = AF_INET;
_sin.sin_port = htons(4567);

代码修订4
accept()的参数3在Unix下为socklen_t*类型,在X code上编译时如果不考虑兼容性会报错。因此为了兼容性,我们修改了下面的代码

#ifdef _WIN32
    _cSock = accept(_sock, (struct sockaddr*)&_clientAddr, &nAddrLen);
#else
    _cSock = accept(_sock, (struct sockaddr*)&_clientAddr, (socklen_t*)&nAddrLen);
#endif

代码修订5
在Windows下,我们的select参数1可以随意设定。但是在Unix下不行,select参数1必须设置为操作的最大文件描述符加1,因此需要重新修改代码。修改如下:

SOCKET maxSock = _sock; //新增
while (true)
{
    //...		
    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];
    }
    //...
    //参数1修改了
    int ret = select(maxSock + 1, &fdRead, &fdWrite, &fdExp, &t);
    //...
}

代码修订6
Windows下fd_set提供fd_count和fd_array数据成员,但是Unix下的fd_set不提供这两个成员,因此代码还需作如下的修改:

//遍历vector数组中所有的客户端套接字,如果某个客户端的套接字在读集中,
//那么说明相应的客户端有数据来,那么就执行processor()函数
for (int n = (int)g_clients.size() - 1; n >= 0; --n)
{
    if (FD_ISSET(g_clients[n], &fdRead))
    {
        if (-1 == processor(g_clients[n]))
        {
            //如果processor出错,那么就将该客户端从全局vector中移除
            //首先获取该套接字在vector中的迭代器位置,然后通过erase()删除
            auto iter = g_clients.begin() + n;
            if (iter != g_clients.end())
            {
                g_clients.erase(iter);
            }
        }
    }
}

最终代码

#ifdef _WIN32
	#define WIN32_LEAN_AND_MEAN
	#define _WINSOCK_DEPRECATED_NO_WARNINGS //for inet_pton()
	#define _CRT_SECURE_NO_WARNINGS
	#include <windows.h>
	#include <WinSock2.h>
	#pragma comment(lib, "ws2_32.lib")
#else
	#include <unistd.h>
	#include <sys/socket.h>
	#include <sys/types.h>
	#include <arpa/inet.h>
	#include <netinet/in.h>
	#include <sys/select.h>
	//在Unix下没有这些宏,为了兼容,自己定义
	#define SOCKET int
	#define INVALID_SOCKET  (SOCKET)(~0)
	#define SOCKET_ERROR            (-1)
#endif
 
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <vector>
 
using namespace std;
 
//消息的类型
enum CMD
{
	CMD_LOGIN,         //登录
	CMD_LOGIN_RESULT,  //登录结果
	CMD_LOGOUT,        //退出
	CMD_LOGOUT_RESULT, //退出结果
	CMD_NEW_USER_JOIN, //新的客户端加入
	CMD_ERROR          //错误
};
 
//数据报文的头部
struct DataHeader
{
	short cmd;        //命令的类型
	short dataLength; //数据的长度
};
 
//登录消息体
struct Login :public DataHeader 
{
	Login() {
		cmd = CMD_LOGIN;
		dataLength = sizeof(Login); //消息长度=消息头(父类)+消息体(子类)
	}
	char userName[32]; //账号
	char PassWord[32]; //密码
};
 
//登录结果
struct LoginResult :public DataHeader
{
	LoginResult():result(0) {
		cmd = CMD_LOGIN_RESULT;
		dataLength = sizeof(LoginResult);
	}
	int result; //登录的结果,0代表正常
};
 
//退出消息体
struct Logout :public DataHeader
{
	Logout() {
		cmd = CMD_LOGOUT;
		dataLength = sizeof(Logout);
	}
	char userName[32]; //账号
};
 
//退出结果
struct LogoutResult :public DataHeader
{
	LogoutResult() :result(0) {
		cmd = CMD_LOGOUT_RESULT;
		dataLength = sizeof(LogoutResult);
	}
	int result; //退出的结果,0代表正常
};
 
//新的客户端加入,服务端给其他所有客户端发送此报文
struct NewUserJoin :public DataHeader
{
	NewUserJoin(int _cSocket=0) :sock(_cSocket) {
		cmd = CMD_NEW_USER_JOIN;
		dataLength = sizeof(LogoutResult);
	}
	int sock; //新客户端的socket
};
 
//存放客户端的套接字
std::vector<SOCKET> g_clients;
 
//参数:客户端的套接字
//功能:服务端调用,函数内与客户端进行数据的交互
int processor(SOCKET _cSock);
 
int main()
{
#ifdef _WIN32
	WORD ver = MAKEWORD(2, 2);
	WSADATA dat;
	WSAStartup(ver, &dat);
#endif
 
	//建立socket
	SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (INVALID_SOCKET == _sock){
		std::cout << "ERROR:建立socket失败!" << std::endl;
	}
	else {
		std::cout << "建立socket成功!" << std::endl;
	}
 
	//初始化服务端地址
	struct sockaddr_in _sin = {};
#ifdef _WIN32
	_sin.sin_addr.S_un.S_addr = inet_addr("192.168.0.105");
	//_sin.sin_addr.S_un.S_addr = INADDR_ANY;
#else
	_sin.sin_addr.s_addr = inet_addr("192.168.0.104");
#endif
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(4567);
 
	//绑定服务端地址
	if (SOCKET_ERROR == bind(_sock, (struct sockaddr*)&_sin, sizeof(_sin))){
		std::cout << "ERROR:绑定地址失败!" << std::endl;
	}
	else {
		std::cout << "绑定地址成功!" << std::endl;
	}
 
	//监听网络端口
	if (SOCKET_ERROR == listen(_sock, 5)) {
		std::cout << "ERROR:监听网络端口失败!" << std::endl;
	}
	else {
		std::cout << "监听网络端口成功!" << std::endl;
	}
 
	//select的参数1要使用
	SOCKET maxSock = _sock;
	//循环处理客户端的数据
	while (true)
	{
		fd_set fdRead;
		fd_set fdWrite;
		fd_set fdExp;
		FD_ZERO(&fdRead);
		FD_ZERO(&fdWrite);
		FD_ZERO(&fdExp);
		FD_SET(_sock, &fdRead);
		FD_SET(_sock, &fdWrite);
		FD_SET(_sock, &fdExp);
		
		//每次select之前,将所有客户端加入到读集中(此处为了演示,只介绍客户端读的情况)
		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];
		}
 
		struct timeval t = { 3,0 };
		int ret = select(maxSock + 1, &fdRead, &fdWrite, &fdExp, &t);
		if (ret < 0)
		{
			std::cout << "select出错!" << std::endl;
			break;
		}
		if (FD_ISSET(_sock, &fdRead))//如果一个客户端连接进来,那么服务端的socket就会变为可读的,此时我们使用accept来接收这个客户端
		{
			FD_CLR(_sock, &fdRead);
 
			//用来保存客户端地址
			struct sockaddr_in _clientAddr = {};
			int nAddrLen = sizeof(_clientAddr);
			SOCKET _cSock = INVALID_SOCKET;
 
			//接收客户端连接
#ifdef _WIN32
			_cSock = accept(_sock, (struct sockaddr*)&_clientAddr, &nAddrLen);
#else
			_cSock = accept(_sock, (struct sockaddr*)&_clientAddr, (socklen_t*)&nAddrLen);
#endif
			if (INVALID_SOCKET == _cSock) {
				std::cout << "ERROR:接收到无效客户端!" << std::endl;
			}
			else {
				//通知其他已存在的所有客户端,有新的客户端加入
				NewUserJoin newUserInfo(static_cast<int>(_cSock));
				for (int n = 0; n < g_clients.size(); ++n)
				{
					send(g_clients[n], (const char*)&newUserInfo, sizeof(newUserInfo), 0);
				}
				
				g_clients.push_back(_cSock); //将客户端的套接字存入vector内
				std::cout << "接受到新的客户端连接,IP=" << inet_ntoa(_clientAddr.sin_addr)
					<< ",Socket=" << static_cast<int>(_cSock) << std::endl;
			}
		}
 
		//遍历vector数组中所有的客户端套接字,如果某个客户端的套接字在读集中,
		//那么说明相应的客户端有数据来,那么就执行processor()函数
		for (int n = (int)g_clients.size() - 1; n >= 0; --n)
		{
			if (FD_ISSET(g_clients[n], &fdRead))
			{
				if (-1 == processor(fdRead.fd_array[n]))
				{
 
				}
			}
		}
 
		std::cout << "空闲时间,处理其他业务..." << std::endl;
	}//end while
 
#ifdef _WIN32
	 //将所有的客户端套接字关闭
	for (int n = (int)g_clients.size() - 1; n >= 0; --n)
	{
		closesocket(g_clients[n]);
	}
 
	//关闭服务端套接字
	closesocket(_sock);
	WSACleanup();
#else
	for (int n = (int)g_clients.size() - 1; n >= 0; --n)
	{
		close(g_clients[n]);
	}
	close(_sock);
#endif
 
	std::cout << "服务端停止工作!" << std::endl;
	getchar();  //防止程序一闪而过
	return 0;
}
 
int processor(SOCKET _cSock)
{
	//设置接收缓冲区,并接收命令
	char szRecv[1024];
	int _nLen = recv(_cSock, szRecv, sizeof(DataHeader), 0);
	if (_nLen < 0) {
		//recv返回-1:代表函数出错
		//recv返回0:代表对方关闭连接
		//在使用的时候,客户端关闭连接,recv也返回负数,因此两者一起使用
		std::cout << "客户端<Socket=" << _cSock << ">:已退出!" << std::endl;
		return -1;
	}
	
	//在此处还应该判断少包黏包的问题,但是现在处于单机处理状态,后面介绍到复杂的消息通信时再介绍
 
	//获取消息中头部中的信息
	DataHeader* header = (DataHeader*)szRecv;
	switch (header->cmd)
	{
		case CMD_LOGIN: //如果是登录
		{
			//接收消息体
			recv(_cSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
			Login *login = (Login*)szRecv;
			std::cout << "客户端<Socket=" << _cSock << ">:CMD_LOGIN,用户名:" << login->userName << ",密码:" << login->PassWord << std::endl;
 
			//此处可以判断用户账户和密码是否正确等等(省略)
 
			//返回登录的结果给客户端
			LoginResult ret;
			send(_cSock, (const char*)&ret, sizeof(ret), 0);
		}
		break;
		case CMD_LOGOUT:  //如果是退出
		{
			//接收消息体
			recv(_cSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
			Logout *logout = (Logout*)szRecv;
			std::cout << "客户端<Socket=" << _cSock << ">:CMD_LOGOUT,用户名:" << logout->userName << std::endl;
 
			//返回退出的结果给客户端
			LogoutResult ret;
			send(_cSock, (const char*)&ret, sizeof(ret), 0);
		}
		break;
		default:  //如果有错误
		{
			DataHeader header = { CMD_ERROR,0 };
			send(_cSock, (const char*)&header, sizeof(header), 0);
		}
		break;
	}
 
	return 0;
}urn 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值