文章目录
- [一、V1 版本 - echo server](#一、V1 版本 - echo server)
- [二、V2 版本 - DictionaryServer](#二、V2 版本 - DictionaryServer)
- [三、V3 版本 - ChatSystemDemo](#三、V3 版本 - ChatSystemDemo)
- 四、windows和linux之间进行通信
一、V1 版本 - echo server
UdpServer
服务端先从网络中读消息然后再写。
初始化套接字(Init)
系统调用socket
创建套接字的系统调用:

参数一:
协议族/域,用来告诉套接字是做本地通信还是网络通信还是其他。

参数二:
套接字类型:

参数三:
默认为0即可。
返回值:

我们能看到返回值竟然是一个文件描述符,所以这正是linux下一切皆文件的体现,网卡本质也是文件,未来网络通信,本质也是文件通信!
现在我们调用socket只是把网络在文件系统中打开了,还需要给文件绑定ip地址和端口号,这就要用到下面的bind。
bind
套接字编程中,bind 是将【IP 地址 + 端口号】绑定到 套接字(socket)文件描述符 上,而不是直接绑定到进程。
在介绍bind之前小编先介绍几个接下来几个常用的结构体:
struct sockaddr 通用地址结构 函数参数通用类型
struct sockaddr_in IPv4 地址结构 实际存放 IP + 端口
struct sockaddr_in6 IPv6 地址结构 IPv6 使用

第一个参数传递socket的返回值。
第二个参数传递一个填充好ip地址和端口号的结构体:

第三个参数传递结构体的大小。
返回值等于0表示绑定成功,不等于0表示失败。
在介绍bind代码代码实现之前小编先介绍两个需要包的头文件:<netinet/in.h>头文件能提供各种数据类型(sockaddr_in就在该头文件中),<arpa/inet.h>能提供各种大小端转化的方法,加上之前介绍的<sys/types.h>和<sys/socket.h>,这四个头文件是我们网络编程时几乎必须包含的四个头文件。
bind代码分两步实现:
1、填充ip和port
在填充ip和port之前,还需要在 struct sockaddr_in 结构体中,将sin_family 字段赋值为 AF_INET,用来告诉操作系统:这个套接字使用 IPv4 地址格式来进行网络通信,后续的 IP 地址和端口号都要按照 IPv4 协议来解析和处理。
首先要创建一个sockaddr_in类型结构体,然后把它的内存空间清零,我们可以用memset,小编介绍一种新方法,用bzero,需要包<strings.h>头文件,它可以将一段指定的内存空间清零。
ip和端口号需要通过命令行参数传递,所以udpserver.cc中的main函数要接受命令行参数,然后创建UdpServer对象时再把接收到的ip和端口号以参数的形式传递。
填入端口号_port时要把小端转大段端htons(_port),主机字节序转网络字节序),因为端口号也要随着报文一起发给对方主机,这样对方主机在回消息时才知道发送方在哪里。(htons中的s表示short,将16 位无符号短整型 从主机字节序转换为网络字节序)
填入ip地址要把可读性好的字符串风格的点分十进制ip转化为4个字节的整数ip,方便网络传输,这里需要用到inet_addr,它不仅可以将字符串风格的点分十进制ip转为整数ip,并且它的返回值已经是网络字节序,所以不需要再通过htons转换。
还需要注意填ip地址时要这样:local.sin_addr.s_addr,因为sin_addr是结构体,C++规定结构体不能整体赋值,所以只能对sin_addr内部的整型变量s_addr赋值。
2、sockaddr_in和socketfd进行bind
调用bind系统调用即可。
开始通信(Start)
目前服务器已经初始化完成,接下来我们就要基于文件描述符来进行通信了。
1、读取数据
因为udp不是面向字节流的,所以不能直接用read,udp有一套自己的读取、发送数据接口:
recvfrom:

参数:
1、接收方文件描述符。
2、输出型参数,用户自定义一段缓冲区,将读到的数据带出。
3、表示参数2用户自定义缓冲区的大小。
4、表示读取策略,默认置为0,表示阻塞读取。
5、输出型参数,是对端主机的ip和端口号信息,是一个sockaddr_in类型的结构体变量,方便我们回消息。(套接字编程:发送数据 = 消息内容 + 发送主体)
6、输入输出型参数,表示参数5的结构体大小,输入表示传入的结构体大小,输出表示实际结构体大小。
返回值:
大于0表示实际读取多少字节,小于0表示读取失败。
2、发送数据
sendto:

参数:
1、发送方文件描述符。这里可以引出一个结论:
udp既可以读时写,也可以写时读,支持全双工通信。
其他参数类似recvfrom,只不过第5个参数是输入型参数,要传递目标主机的sockaddr_in结构体,还要注意第六个参数是整形类型,不是recvfrom的指针类型。
服务端源码-V1
cpp
//UdpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <cstdlib>
#include "Logger.hpp"
static const int gdefaultsockfd = -1;
class UdpServer
{
public:
UdpServer(const std::string &ip, const int port)
:_ip(ip)
,_port(port)
,_sockfd(gdefaultsockfd)
{
}
void Init()
{
// 1、创建套接字fd
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd < 0)
{
//创建套接字失败
LOG(LogLevel::FATAL) << "create socket error";
exit(1);
}
LOG(LogLevel::INFO) << "create socket sucess: " << _sockfd;
// 2、bind
// 2.1 填充ip和port
struct sockaddr_in local;
bzero(&local, sizeof(local));
//用来告诉操作系统:这个套接字使用IPv4地址格式来进行网络通信
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip.c_str());
// 2.2sockaddr_in和socketfd进行bind
int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if(n < 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(2);
}
LOG(LogLevel::INFO) << "bind socket sucess: " << _sockfd;
}
void Start()
{
_isrunning = true;
while(_isrunning)
{
char buffer[1024];
buffer[0] = 0; //清空缓冲区
sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr *)&peer, &len);
if(n > 0)
{
buffer[n] = 0; //给接收到的字符串的末尾字符的下一个位置写/0
LOG(LogLevel::DEBUG) << "client say# " << buffer;
std::string echo_string = "server echo# ";
echo_string += buffer;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0,
(struct sockaddr *)&peer, len);
}
}
_isrunning = false; //以防服务器自己跑出来
}
void Stop()
{
_isrunning = false;
}
~UdpServer()
{}
private:
int _sockfd;
uint16_t _port;
std::string _ip;
bool _isrunning;
};
cpp
//UdpServer.cc
#include "UdpServer.hpp"
#include <iostream>
#include <memory>
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " localip localpoat" << std::endl;
}
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(0);
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
EnableConsoleLogStrategy();
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip, port);
usvr->Init();
usvr->Start();
return 0;
}
UdpClient
客户端对于服务端来说是客户端先开口说话,所以客户端先写数据到网络里然后再读。
写客户端大致流程和服务端类似,但是有下面几个注意事项:
1、客户端不需要日志系统,直接输出即可。
2、客户端创建sockfd后不用显示的调用bind绑定自己的ip和端口号,当客户端第一次发送消息后会由OS自动将客户端sockfd和本台主机的ip绑定,并随机分配一个没有被使用的端口号,所以客户端直接发消息即可,因为对于客户端来说,会有各种app,如微信、抖音、支付宝,这些公司不会互相协商对端口号的绑定规则,所以如果让客户端程序显示绑定端口号,就有可能两个进程同时绑定一个端口号,势必会造成一个进程无法启动。而对于服务端来说,可以显示的绑定自己的ip和端口号,因为服务端都是属于同一家公司,公司内部可以对端口号的使用做分配。
3、客户端中一般会内置服务端的ip和端口号,这里我们也用命令行参数来传递服务端的ip和端口号。
源码
cpp
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
void Usage(std::string proc)
{
//proc表示类似于 ./client
std::cerr << "Usage: " << proc << "localip localpoat" << std::endl;
}
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(0);
}
//提取命令行参数
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cout << "create socket error" << std::endl;
return 0;
}
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
//用来告诉操作系统:这个套接字使用IPv4地址格式来进行网络通信
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
while(true)
{
std::cout << "Please Enter@ ";
std::string line;
std::getline(std::cin, line); //读取客户端输入并转化为字符串
//写
sendto(sockfd, line.c_str(), line.size(), 0,
(struct sockaddr *)&server, sizeof(server));
//读
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
int m = recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&temp, &len);
if(m > 0)
{
//读成功
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
测试代码注意事项
1、现在我们客户端和服务端都写完了,测试服务端时命令行参数ip填127.0.0.1,这是本地环回主机ip,专门用来本地测试和本地通信,端口号随便填。
2、当我们启动服务器后,想验证服务器是否启动可以用 netstat 命令,用来查看网络状态,-n表示查udp服务器,-ua表示查所有udp服务器,-n表示把能显示成数字的信息显示为数字,-p表示显示更多进程信息。
3、云服务器禁止用户bind公网ip,即使是能bind公网ip或任意ip,最佳实践还是不建议服务端bind固定ip! 而是任意ip bind,操作如下所示:
cpp
local.sin_addr.s_addr = htonl(INADDR_ANY);
这样只要是发给这台主机的8080端口号的信息(如该主机的环回ip 127.0.0.1 和 该主机的公网ip如1.95.35.127),服务端就都能接收,如果服务端绑定了固定ip,那么该服务端就只能收到发给该固定ip的消息。所以服务端不用再通过命令行参数传递ip了,服务端的只用接收端口号即可。
完整源码
cpp
//udpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <cstdlib>
#include "Logger.hpp"
static const int gdefaultsockfd = -1;
class UdpServer
{
public:
UdpServer(const int port)
:_port(port)
,_sockfd(gdefaultsockfd)
{
}
void Init()
{
// 1、创建套接字fd
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd < 0)
{
//创建套接字失败
LOG(LogLevel::FATAL) << "create socket error";
exit(1);
}
LOG(LogLevel::INFO) << "create socket sucess: " << _sockfd;
// 2、bind
// 2.1 填充ip和port
struct sockaddr_in local;
bzero(&local, sizeof(local));
//用来告诉操作系统:这个套接字使用IPv4地址格式来进行网络通信
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = htonl(INADDR_ANY); //任意ip bind
//local.sin_addr.s_addr = inet_addr(_ip.c_str());
// 2.2sockaddr_in和socketfd进行bind
int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if(n < 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(2);
}
LOG(LogLevel::INFO) << "bind socket sucess: " << _sockfd;
}
void Start()
{
_isrunning = true;
while(_isrunning)
{
char buffer[1024];
buffer[0] = 0; //清空缓冲区
sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr *)&peer, &len);
if(n > 0)
{
//获取客户端ip和端口号
uint16_t clientport = ntohs(peer.sin_port);
std::string clientip = inet_ntoa(peer.sin_addr);
buffer[n] = 0; //给接收到的字符串的末尾字符的下一个位置写/0
LOG(LogLevel::DEBUG) << "[" << clientip << ":" <<
clientport << "]# " << buffer;
std::string echo_string = "server echo# ";
echo_string += buffer;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0,
(struct sockaddr *)&peer, len);
}
}
_isrunning = false; //以防服务器自己跑出来
}
void Stop()
{
_isrunning = false;
}
~UdpServer()
{}
private:
int _sockfd;
uint16_t _port;
//std::string _ip; //
bool _isrunning;
};
cpp
//udpserver.cc
#include "UdpServer.hpp"
#include <iostream>
#include <memory>
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " localpoat" << std::endl;
}
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = std::stoi(argv[1]);
EnableConsoleLogStrategy();
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
usvr->Init();
usvr->Start();
return 0;
}
cpp
//udpclient.cc
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
void Usage(std::string proc)
{
//proc表示类似于 ./client
std::cerr << "Usage: " << proc << "localip localpoat" << std::endl;
}
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(0);
}
//提取命令行参数
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cout << "create socket error" << std::endl;
return 0;
}
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
//用来告诉操作系统:这个套接字使用IPv4地址格式来进行网络通信
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
while(true)
{
std::cout << "Please Enter@ ";
std::string line;
std::getline(std::cin, line); //读取客户端输入并转化为字符串
//写
sendto(sockfd, line.c_str(), line.size(), 0,
(struct sockaddr *)&server, sizeof(server));
//读
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
int m = recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&temp, &len);
if(m > 0)
{
//读成功
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
二、V2 版本 - DictionaryServer
EchoServer主要解决的是io问题,现在我们要给服务端添加一些业务,可以完成翻译功能,可以把客户端发送给服务端的信息做翻译。
1、首先我们要理解服务端DictServer本身只用承担发送数据和接收数据的功能,具体业务逻辑是需要另外写一段程序的,然后由服务端回调执行后再返回,所以我们要把软件进行分层实现,Dictionary.hpp负责翻译,DictServer.hpp负责网络IO。
源码
cpp
//Dictionary.hpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Logger.hpp"
//定义分隔符
const static std::string sep = ":";
class Dictionary
{
private:
//加载_path中的配置文件并初始化unordered_map
void LoadConf()
{
//以只读模式打开 _path 路径对应的文件,并将其绑定到 in 这个输入流对象上,
//后续就可以通过 in 像 std::cin 一样读取文件内容
std::ifstream in(_path);
if(!in.is_open())
{
LOG(LogLevel::ERROR) << "open file error: " << _path;
return;
}
std::string line;
while(std::getline(in, line))
{
LOG(LogLevel::DEBUG) << "load dict nessage: " << line;
//拆分字符串line,分别插入unordered_map: dog:狗
auto pos = line.find(sep);
if(pos == std::string::npos)
{
//没找到分隔符
LOG(LogLevel::ERROR) << "format error:" << line;
continue; //只是这一行字符串没有分隔符,还需要继续加载后续的字符串
}
std::string word = line.substr(0, pos); //起始位置,截取长度
std::string value = line.substr(pos + sep.size()); //不传第二个参数,默认截取到结尾
if(word.empty() || value.empty())
{
LOG(LogLevel::ERROR) << "format error, word or value is empty: " << line;
continue;
}
_dict.insert({word, value});
}
in.close();
}
public:
Dictionary(const std::string &path)
:_path(path)
{
LOG(LogLevel::INFO) << "construct dictionary obj";
LoadConf();
}
std::string Translate(const std::string &word, const std::string &whoip, uint16_t whoport)
{
//该接口不负责收发数据,故不用参数whoip和whoport
(void)whoip;
(void)whoport;
auto iter = _dict.find(word);
if(iter == _dict.end())
{
//没找到
return "unknown";
}
//找到了
return iter->first + "->" + iter->second;
}
~Dictionary()
{}
private:
std::string _path;
std::unordered_map<std::string, std::string> _dict;
};
cpp
//DictServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <cstdlib>
#include <functional>
#include "Logger.hpp"
using callback_t = std::function<std::string (const std::string &word,
const std::string &whoip, uint16_t whoport)>;
static const int gdefaultsockfd = -1;
class DictServer
{
public:
DictServer(const int port, callback_t cb)
:_port(port)
,_sockfd(gdefaultsockfd)
,_cb(cb)
{
}
void Init()
{
// 1、创建套接字fd
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd < 0)
{
//创建套接字失败
LOG(LogLevel::FATAL) << "create socket error";
exit(1);
}
LOG(LogLevel::INFO) << "create socket sucess: " << _sockfd;
// 2、bind
// 2.1 填充ip和port
struct sockaddr_in local;
bzero(&local, sizeof(local));
//用来告诉操作系统:这个套接字使用IPv4地址格式来进行网络通信
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = htonl(INADDR_ANY); //任意ip bind
//local.sin_addr.s_addr = inet_addr(_ip.c_str());
// 2.2sockaddr_in和socketfd进行bind
int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if(n < 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(2);
}
LOG(LogLevel::INFO) << "bind socket sucess: " << _sockfd;
}
void Start()
{
_isrunning = true;
while(_isrunning)
{
char buffer[1024];
buffer[0] = 0; //清空缓冲区
sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr *)&peer, &len);
if(n > 0)
{
buffer[n] = 0; //转C语言风格字符串
//获取客户端ip和端口号
uint16_t clientport = ntohs(peer.sin_port);
std::string clientip = inet_ntoa(peer.sin_addr);
std::string word = buffer;
LOG(LogLevel::DEBUG) << "用户查找: " << word;
std::string result = _cb(word, clientip, clientport);
// buffer[n] = 0; //给接收到的字符串的末尾字符的下一个位置写/0
// LOG(LogLevel::DEBUG) << "[" << clientip << ":" <<
// clientport << "]# " << buffer;
// std::string echo_string = "server echo# ";
// echo_string += buffer;
sendto(_sockfd, result.c_str(), result.size(), 0,
(struct sockaddr *)&peer, len);
}
}
_isrunning = false; //以防服务器自己跑出来
}
void Stop()
{
_isrunning = false;
}
~DictServer()
{}
private:
int _sockfd;
uint16_t _port;
//std::string _ip; //
callback_t _cb;
bool _isrunning;
};
cpp
//DictServer.cc
#include "Dictionary.hpp" //负责翻译
#include "DictServer.hpp" //负责网络IO
#include <iostream>
#include <memory>
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " localpoat" << std::endl;
}
// std::string Translate(const std::string &word, const std::string &whoip, uint16_t whoport)
// {
// return "哈哈";
// }
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
EnableConsoleLogStrategy();
Dictionary dict("./dict.txt");
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<DictServer> usvr = std::make_unique<DictServer>(port,
[&dict](const std::string &word, const std::string &whoip, uint16_t whoport)->std::string{
return dict.Translate(word, whoip, whoport);
});
usvr->Init();
usvr->Start();
return 0;
}
cpp
//DictClient.cc
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
void Usage(std::string proc)
{
//proc表示类似于 ./client
std::cerr << "Usage: " << proc << "localip localpoat" << std::endl;
}
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(0);
}
//提取命令行参数
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cout << "create socket error" << std::endl;
return 0;
}
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
//用来告诉操作系统:这个套接字使用IPv4地址格式来进行网络通信
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
while(true)
{
std::cout << "Please Enter@ ";
std::string line;
std::getline(std::cin, line); //读取客户端输入并转化为字符串
//写
sendto(sockfd, line.c_str(), line.size(), 0,
(struct sockaddr *)&server, sizeof(server));
//读
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
int m = recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&temp, &len);
if(m > 0)
{
//读成功
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
三、V3 版本 - ChatSystemDemo
前面我们实现的V2版本的服务器是单进程版本,效率比较低,我们先实现更高效的服务器我们使用线程池,服务器将任务打包派发给线程池,然后由线程池中的现场完成任务,这样就可以实现多个线程一边处理任务,服务器一边从客户端读取数据,使得生产和消费并发运行。

但是我们接下来不再实现多线程的DictServer,而是实现一个多用户聊天系统。
cpp
//ChatServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <cstdlib>
#include <functional>
#include "InetAddr.hpp"
#include "Logger.hpp"
#include "Route.hpp"
//此处必须message和add必须拷贝传参,因为他们本质是临时变量
using callback_t = std::function<void (int sockfd,
std::string message, InetAddr &addr)>;
static const int gdefaultsockfd = -1;
class ChatServer
{
public:
ChatServer(const int port, callback_t cb)
:_port(port)
,_sockfd(gdefaultsockfd)
,_cb(cb)
{
}
void Init()
{
// 1、创建套接字fd
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd < 0)
{
//创建套接字失败
LOG(LogLevel::FATAL) << "create socket error";
exit(1);
}
LOG(LogLevel::INFO) << "create socket sucess: " << _sockfd;
// 2、bind
// 2.1 填充ip和port
InetAddr local(_port);
// 2.2sockaddr_in和socketfd进行bind
int n = bind(_sockfd, (struct sockaddr*)&local, local.Lenth());
if(n < 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(2);
}
LOG(LogLevel::INFO) << "bind socket sucess: " << _sockfd;
}
void Start()
{
_isrunning = true;
while(_isrunning)
{
char buffer[1024];
buffer[0] = 0; //清空缓冲区,默认当前buffer为空
sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr *)&peer, &len);
if(n > 0)
{
//约定:buffer中是聊天消息
buffer[n] = 0; //转C语言风格字符串
//获取对应Client
InetAddr Clientaddr(peer);
LOG(LogLevel::DEBUG) << "get a client info: # "
<< Clientaddr.Ip() << " - " << Clientaddr.Port() << ": "
<< buffer;
std::string message = buffer;
//回调(将Clientaddr发来的message通过_sockfd转发给线程池中每一个线程)
_cb(_sockfd, message, Clientaddr);
// 服务器目前只需收消息然后上层回调,回消息由线程池完成
// sendto(_sockfd, result.c_str(), result.size(), 0,
// (struct sockaddr *)&peer, len);
}
}
_isrunning = false; //以防服务器自己跑出来
}
void Stop()
{
_isrunning = false;
}
ChatServer()
{}
private:
int _sockfd;
uint16_t _port;
callback_t _cb;
bool _isrunning;
};
封装 sockaddr_in,简化 Linux 网络编程的地址处理:
cpp
//InetAddr.hpp
#pragma once
// 用来描述client socket信息的类
// 方便后续用来管理客户端
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
//定义一个宏
#define Conv(addr) ((struct sockaddr*)&addr)
class InetAddr
{
private:
void Net2Host()
{
_port = ntohs(_addr.sin_port);
_ip = inet_ntoa(_addr.sin_addr);
}
void Host2Net()
{
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
_addr.sin_addr.s_addr = inet_addr(_ip.c_str());
}
public:
InetAddr(const sockaddr_in &addr)
: _addr(addr)
{
Net2Host();
}
// 重载构造函数
// ip缺省值为"0.0.0.0",经过inet_addr转化后就是INADDR_ANY
InetAddr(uint16_t port, const std::string ip = "0.0.0.0")
: _port(port), _ip(ip)
{
Host2Net();
}
//get方法
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
struct sockaddr* Addr()
{
return Conv(_addr);
}
socklen_t Lenth()
{
return sizeof(_addr);
}
//用于比较两个用户地址是否相等
bool operator==(const InetAddr &addr)
{
return (_ip == addr._ip && _port == addr._port);
}
//for debug _addr信息转为字符串
std::string ToString()
{
return _ip + "-" + std::to_string(_port);
}
~InetAddr()
{
}
private:
// 网络风格地址
sockaddr_in _addr;
// 主机风格地址
std::string _ip;
uint16_t _port;
};
管理在线用户 + 把一个人发的消息转发给所有人:
cpp
//Route.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"
// 用于维护所有在线用户
class Route
{
private:
//用于判断用户是否存在
bool IsExists(const InetAddr &addr)
{
for(auto &user : _online_user)
{
if(user == addr)
{
return true;
}
}
return false;
}
void AddUser(const InetAddr &addr)
{
if(!IsExists(addr))
_online_user.push_back(addr);
}
void SendMessageToAll(int sockfd, std::string &message, InetAddr addr)
{
for(auto &user : _online_user)
{
LOG(LogLevel::DEBUG) << "route [" << message << "] to " << user.ToString();
std::string info = addr.ToString();
info += "# ";
info += message;
sendto(sockfd, info.c_str(), info.size(), 0, user.Addr(), user.Lenth());
}
}
void DeleteUser(std::string &message, InetAddr addr)
{
//权宜之计,后续用协议判断才是最佳实践
//这里应该用message和"QUIT"进行比较,而不是message.c_str()
if(message == "QUIT")
{
//!这里不能用范围for,在范围for里erase会导致迭代器失效!
auto iter = _online_user.begin();
//迭代器遍历应该用!=,而不是<=
for(; iter != _online_user.end(); iter++)
{
if(*iter == addr)
{
_online_user.erase(iter);
break;
}
}
}
}
public:
Route()
{
}
void RouteMessageToAll(int sockfd, std::string &message, InetAddr addr)
{
AddUser(addr);
SendMessageToAll(sockfd, message, addr);
DeleteUser(message, addr);
}
// void RouteMessageToOne(int sockfd, std::string &message, InetAddr addr)
// {
// }
~Route()
{
}
private:
//临界资源
std::vector<InetAddr> _online_user;
};
cpp
//ServerMain.cc
#include "ThreadPool.hpp"
#include "Route.hpp"
#include "ChatServer.hpp"
#include <iostream>
#include <memory>
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " localpoat" << std::endl;
}
//线程池任务默认无参无返回值
using task_t = std::function<void()>;
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
EnableConsoleLogStrategy();
uint16_t port = std::stoi(argv[1]);
//1、消息转发功能
std::unique_ptr<Route> r = std::make_unique<Route>();
//2、定义线程池对象
ThreadPool<task_t> *tp = ThreadPool<task_t>::GetInstance();
std::unique_ptr<ChatServer> usvr = std::make_unique<ChatServer>(port,
[&r, &tp](int sockfd, std::string message, InetAddr &addr){
task_t task = std::bind(&Route::RouteMessageToAll, r.get(),
sockfd, message, addr);
tp->Enqueue(task);
});
usvr->Init();
usvr->Start();
return 0;
}
修改客户端:
目前客户端存在一个问题,因为客户端是单线程的,所以当用户不发消息时客户端线程会在sendto处被阻塞,那么就无法通过recvfrom读取其他用户发的消息,所以需要对服务器进行改进,就算客户端用户不发消息也能看到其他用户的消息。
解决方案是在客户端也引入多线程,让线程并发执行写sendto和读recvfrom,这就是udp的全双工应用表现。
这里我们用C++内部的thread来实现客户端多线程。
cpp
//ChatClient.cc
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <thread>
void Usage(std::string proc)
{
// proc表示类似于 ./client
std::cerr << "Usage: " << proc << "localip localpoat" << std::endl;
}
int sockfd = -1;
std::string serverip;
uint16_t serverport;
void InitClient(const std::string &serverip, uint16_t serverport)
{
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cout << "create socket error" << std::endl;
}
}
void recver()
{
while (true)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
int m = recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr *)&temp, &len);
if (m > 0)
{
// 读成功
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
}
void sender()
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
// 用来告诉操作系统:这个套接字使用IPv4地址格式来进行网络通信
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
while (true)
{
std::cout << "Please Enter@ ";
std::string line;
std::getline(std::cin, line); // 读取客户端输入并转化为字符串
// 写
sendto(sockfd, line.c_str(), line.size(), 0,
(struct sockaddr *)&server, sizeof(server));
}
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
// 提取命令行参数
serverip = argv[1];
serverport = std::stoi(argv[2]);
// 创建客户端
InitClient(serverip, serverport);
// 创建两个线程
std::thread recv(recver);
std::thread send(sender);
// 线程等待
recv.join();
send.join();
return 0;
}
四、windows和linux之间进行通信
我们前面已经实现了用linux做客户端和服务端进行通信,但是我们现实中还是windows做客户端和linux做服务端更常见,所以我们接下来介绍windows和linux之间如何进行网络通信。
其实实现windows和linux之间的网络通信很简单,因为我们知道windows和linux共享同一套网络协议栈,所以我们可以提前下结论,当两个平台进行通信时,它们的socket接口基本是一样的。

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~
