目录
一、echo_UdpServer
使用UDP协议进行网络通信,用代码实现此功能
将服务端server布置在云服务器上,用一个头文件封装服务端的接口,用server.cc和client.cc模拟网络通信
先将之前写的日志包进来,这样通信实现中的错误就可以打印;日志的实现需要锁,因此也要将锁的实现包进来
首先在server中要进行套接字的申请,即要先实现Init,在这个函数中申请一个套接字,相当于文件描述符并且这个描述符在通信中要被使用,因此将其设置为成员属性_sockfd,申请的实际上是一个网络文件,要使用接口
// 这是需要的头文件
#include <sys/types.h>
#include <sys/socket.h>// 这里是接口,domain是域,这里是网络通信因此设置为AF_INET(AF表示协议家族,INET表示在网络中);type表示通信的数据类型,UDP面向数据报,因此设置为SOCK_DGRAM;protocol表示使用的协议类型,可以设置为0,因为由前两个参数已经可以确认这就是UDP协议
int socket(int domain, int type, int protocol);
RETURN VALUE
On success, a file descriptor for the new socket is returned. On error, -1 is returned, and
errno is set appropriately.
当然可能申请失败,此时利用日志打印错误 LOG(LogLevel::FATAL) << "socket error!" 并且exit(1);申请成功就打印成功日志以及_sockfd
接下来绑定自己的端口号以及ip,使对方可以确认通信进程
需要使用接口
// 头文件
#include <sys/types.h>
#include <sys/socket.h>// 接口,参数解释:第一个就是申请的描述符,第二个就是网络基础中阐述的结构体(继承多态),第三个是这个结构体的大小
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
由于这个结构体是一个输入型参数,我们需要自己定义,并且ip和端口号就是绑定在这里面的,也就是ip和端口号都在里面设置,因为是网络通信,定义的结构体类型应该是struct sockaddr_in;最后一个参数是结构体的大小
介绍这个结构体(下面块引用)->源码定义
因为ip和端口号要在通信中传给对方因此也要设置为成员属性
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.

// sockaddr_in介绍
这是一个结构体,第一个参数是一个宏定义,将sa_prefix定义为sa_prefix&&family,&&在宏定义中的作用就是将前后两者连接,那么就是sa_prefix_family,在结构体中就是sin_family,类型是协议家族sa_family_t,使用udp的时候初始化为AF_INET
第二个参数是端口号,类型实际上就是无符号短整型
第三个参数是ip地址,这个结构体中只有一个32位长整型类型变量,也就是ip
最后一个参数是填充字段,不用管
填充 sockaddr_in local 结构体,首先要将结构体清空,可以使用memset,这里介绍一个新的方法:bzero
#include <strings.h>
void bzero(void *s, size_t n);
// 用0清空s中的数据,n表示s的大小
这部分写代码
注意成员变脸端口号传入网络要转换成网络序列,使用函数htons;成员变量ip地址是用字符串接收的,所以传入网络的时候要转换,首先转为整数,之后转为网络序列,使用一个接口可以一步做到in_addr_t inet_addr(const char *cp)
将ip和端口号设置为成员属性,并且要求构造函数传入,在server.cc启动的时候初始化服务端的ip和端口号

接下来就是Start的编写了
实际上所有的软件都是自死循环,因为其服务器不会停止,那么这里的start启动服务器就让其一直死循环,要设置一个属性_isrunning,初始化为false,Start内部置为true,并且以这个变量为判断条件循环
接下来server要接受来自客户端的消息,需要认识接口recvfrom
#include <sys/types.h>
#include <sys/socket.h>ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
// 第一个参数就是自己的套接字描述符;// 第二个参数是缓冲区,和文件一样,读取数据要先放入缓冲区;
// 第三个参数是缓冲区的大小;
// 第四个参数是读取方式,设置为0表示阻塞式IO
// 第五个参数是一个输出型参数,先定义一个struct sockaddr_in peer,这个函数接收来自客户端的数据,客户端的那个绑定ip以及端口需要的结构体会被传过来,并且记录在src_addr中,我们用peer(表示远端)来接受,这样server就知道了数据的来源
// 第六个参数是输出输入型参数,表示实际读到的数据大小,需要定义一个len(输入体现在len = sizeof(peer)即要先初始化)传入之后接收,len的值会改变为对方结构体的大小
RETURN VALUE
These calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate the error.
// 接收成功返回读到的信息是多少字节,否则-1
若是返回值大于0,那么说明接收成功,server先打印接收到哪一个客户端以及其发送的信息(先不考虑来自哪一个客户端),server之后要返回信息表示成功接收到了,这里使用接口sendto
#include <sys/types.h>
#include <sys/socket.h>ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
// 第一个参数为套接字标识符// 第二个是发送信息存的缓冲区
// 第三个是缓冲区大小
// 第四个表示发送方式,设置为0表示阻塞式IO
// 第五个是结构体,通过此结构体就能知道目的进程的ip以及端口号,这也是为什么接收信息时要记录下对方的结构体
// 从这里也能知道udp的sockfd是一个即可以写又可以读的,是全双工的
// 第六个参数是结构体大小
RETURN VALUE
On success, these calls return the number of bytes sent. On error, -1 is returned, and errno is set appropriately.
代码
打印接收到的信息的时候是日志的DEBUG类型
发送的消息要在buffer原有的内容前面加上一个server echo# 表示是服务器的响应

接下来是server.cc的代码
首先./server的时候要传入ip以及端口号,那么就要使用命令行参数的形式,因此要先判断命令行参数的个数是不是对的,若是不对应该打印要如何输入并且退出,强制你重新./server
之后记录下ip和端口号,再将日志的打印模式设置为向显示器打印,然后用智能指针定义一个UdpServer类,之后调用方法Init以及Start

server的已经基本完成,现在写client.cc,这个没有封装;
首先也需要传递server的ip以及端口号,因为client发送消息要知道server的进程是哪一个;之后申请自己的套接字,之后需要绑定吗?
答案是不需要绑定,这是因为操作系统会自动为客户端的 UDP Socket 分配临时的源 IP 和端口,手动绑定反而会增加不必要的复杂度,甚至引发端口冲突问题。
之后获取server的ip以及端口号,并且初始化结构体
之后开始通信,也是一个死循环。client先发消息,之后才接受消息,接收消息的结构体是一个输出型参数,可以用peer来接收,这个结构体里面就是server的结构体

开始测试
先启动server连接公网ip,发现连接不了;

启动server连接内网ip或者127.0.0.1,可以绑定,之后启动client使用相同的ip、port,可以进行通信


127.0.0.1是一个本地环回测试的ip,要求cs在同一台机器上,c发出的消息不会传递到网络,而是传到OS,之后转一圈到s,目的是测试代码,因为还没有提交到网络,若是测试有误则说明代码有问题,而不是网络
使用ifconfig可以查看内网ip
启动server连接内网ip或者127.0.0.1,启动client连接127.0.0.1或者内网ip发现无法通信,这是因为:使用netstat -anup(a表示所有,n表示转为数字,u表示udp,p表示进程)查看,发现server绑定了内网或者127.0.0.1而client发送的目的却不是这个ip,所以才无法通信
总结:server无法绑定公网ip,绑定内网或者本地换回并且client也一样可以通信,不一样无法通信;
基于这个总结,那么应该如何跨网络通信呢?实际上server不建议手动绑定特定的ip,否则server只能和固定的主机通信,若是现在有很多主机都向server的同一端口发消息,而server只绑定了这个端口的一个客户端,那么就只能和这个客户端通信,其他的不行。一般将server绑定的ip设置为INADDR_ANY(宏替换为0)表示绑定任意地址ip,这样就可以和多个client通信
此时netstat -anup查看,发现server进程的ip为0.0.0.0,那么就可以接收任何ip发来的消息了

对应地,修改server.hpp的代码,去掉ip,只绑定INADDR_ANY,同样地server.cc的代码也要改变,这是修改部分的代码
// server.hpp
#pragma once
#include <iostream>
#include "Log.hpp"
#include "mutexEncapsulation.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <string>
using namespace LogModule;
const int defaultsockfd = -1;
class UdpServer
{
public:
UdpServer(uint16_t port)
: _sockfd(defaultsockfd),
_port(port),
_isrunning(false)
{
}
void Init()
{
// 申请套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
logger(LogLevel::FATAL) << "socket error!";
exit(1);
}
logger(LogLevel::INFO) << "socket success, _sockfd : " << _sockfd;
// 绑定ip、端口
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(_port);
addr.sin_addr.s_addr = INADDR_ANY;
int n = bind(_sockfd, (struct sockaddr *)&addr, sizeof(addr));
if (n == -1)
{
logger(LogLevel::FATAL) << "bind error!";
exit(2);
}
logger(LogLevel::INFO) << "bind success, _sockfd : " << _sockfd;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&peer, &len);
if (s > 0)
{
buffer[s] = 0;
std::cout << "buffer : " << buffer << std::endl;
std::string echo_string = "server echo# ";
echo_string += buffer;
ssize_t n = sendto(_sockfd, echo_string.c_str(),
echo_string.size(), 0, (struct sockaddr*)&peer, len);
}
}
}
~UdpServer()
{
}
private:
int _sockfd;
uint16_t _port;
bool _isrunning;
};
// server.cc
#include "server.hpp"
#include <memory>
// ./server port
int main(int argc, char* argv[])
{
if (argc != 2)
{
std::cerr << "Usage : " << argv[0] << " port" << std::endl;
return 1;
}
uint16_t port = std::stoi(argv[1]);
Enable_ConsoleLog_Strategy();
std::unique_ptr<UdpServer> usevr = std::make_unique<UdpServer>(port);
usevr->Init();
usevr->Start();
return 0;
}
重新做上述实验,server只需要绑定端口,client用公网、内网、本地环回都可以进行通信,测试如下

这是在一台主机上的测试,跨网络可以在另一台主机上跑client程序,告诉client server所在机器的ip以及server代码进程绑定的端口同样可以通信
server想要知道是哪个client发送的消息,可以在server.cc中收到消息之后,打印client的ip(使用inet_ntoa(struct in_addr in)接口将4字节网络风格的IP转换为点分十进制的字符串风格的IP)和端口(要转换网络序列),因为peer在recvfrom中已经知道了客户端的结构体,这是代码修改

运行代码试试

可以看出先客户端口是自动分配的,就是为了避免冲突
二、Dict_UdpServer
v2版本是实现一个翻译功能的通信,也就是客户端发送消息给服务端,服务端拿着这个字符串去字典里面找到对应的翻译之后再将这个翻译的消息发送给客户端
首先要实现一个字典类Dict,这个类包方法DictLoad(加载字典将翻译的映射关系填入一个哈希表,这个字典是一个文件)、Translate(返回对应单词的意思)。那么就要设置一个属性unordered_map<string, string> trans
之后在server.cc里面实例化一个Dick的对象,将这个对象的translate功能通过lambda表达式传递给server.hpp中的UdpServer类,通过function类型属性包装这个方法(实现了Dict和UdpServer两个类之间的关联,这是低耦合的),在server收到消息之后就将英文单词传给这个func成员变量去调用translate并且将结果传给客户端,client.cc打印这个翻译结果
这是代码
词典信息
apple: 苹果
banana: 香蕉
orange: 橙子
grape: 葡萄
pear: 梨
peach : 桃子
lemon: 柠檬
mango : 芒果
strawberry: 草莓
watermelon: 西瓜
pineapple: 菠萝
cherry: 樱桃
kiwi: 猕猴桃
coconut: 椰子
blueberry: 蓝莓
Dict类
// 这个头文件封装Dict类
#pragma once
#include <iostream>
#include <unordered_map>
#include <string>
#include <fstream>
#include "Log.hpp"
using namespace LogModule;
std::string dict_path = "./Dictionary";
const std::string sep = ": "; // 字典中每一对翻译的中英字符串分隔符
class Dict
{
public:
Dict(std::string &path = dict_path) : _path(path)
{
}
bool DictLoad()
{
// 根据路径打开文件
std::ifstream in(_path);
if (!in.is_open())
{
logger(LogLevel::ERROR) << "打开字典:" << _path << " 错误";
return false;
}
// 读取&&分割文件->getline逐行读取
std::string line;
while (std::getline(in, line))
{
int pos = line.find(sep);
// 解析失败不应该退出而是跳过这行接续接续,因此日志等级为WARNING
if (pos == std::string::npos)
{
logger(LogLevel::WARNING) << "解析 " << line << " 失败";
continue;
}
std::string english = line.substr(0, pos);
std::string chinese = line.substr(pos + sep.size());
// 解析失败
if (english.empty() || chinese.empty())
{
logger(LogLevel::WARNING) << "没有有效内容:" << line;
continue;
}
// 解析成功,记录对应关系到_trans哈希表
_trans.insert({english, chinese});
logger(LogLevel::INFO) << "加载:" << line;
}
return true;
}
std::string Translate(std::string &word)
{
// 若是没有这个对应关系,直接返回
if (_trans.count(word) == 0)
{
logger(LogLevel::DEBUG) << "翻译失败,词典中找不到意思";
return "None";
}
logger(LogLevel::INFO) << "翻译成功,意思是:" << _trans[word];
return _trans[word];
}
~Dict()
{
}
private:
std::string _path;
std::unordered_map<std::string, std::string> _trans;
};
server.hpp
#pragma once
#include <iostream>
#include "Log.hpp"
#include "mutexEncapsulation.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <string>
#include <functional>
using namespace LogModule;
const int defaultsockfd = -1;
// 声明类型
typedef std::function<std::string(std::string&)> func_t;
class UdpServer
{
public:
UdpServer(uint16_t port, func_t func)
: _sockfd(defaultsockfd),
_port(port),
_isrunning(false),
_func(func)
{
}
void Init()
{
// 申请套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
logger(LogLevel::FATAL) << "socket error!";
exit(1);
}
logger(LogLevel::INFO) << "socket success, _sockfd : " << _sockfd;
// 绑定ip、端口
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(_port);
addr.sin_addr.s_addr = INADDR_ANY;
int n = bind(_sockfd, (struct sockaddr *)&addr, sizeof(addr));
if (n == -1)
{
logger(LogLevel::FATAL) << "bind error!";
exit(2);
}
logger(LogLevel::INFO) << "bind success, _sockfd : " << _sockfd;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&peer, &len);
if (s > 0)
{
buffer[s] = 0;
// uint16_t client_port = ntohs(peer.sin_port);
// std::string client_ip = inet_ntoa(peer.sin_addr);
// std::cout << "[ " << client_ip << " | " << client_port << " ]# " << buffer << std::endl;
// std::string echo_string = "server echo# ";
// echo_string += buffer;
std::string word = buffer;
std::string result = _func(word);
ssize_t n = sendto(_sockfd, result.c_str(),
result.size(), 0, (struct sockaddr *)&peer, len);
}
}
}
~UdpServer()
{
}
private:
int _sockfd;
uint16_t _port;
bool _isrunning;
func_t _func;
};
server.cc变动

测试结果

可以设置一个InetAddr类来获取请求翻译的客户端的ip以及端口号。这个类里面通过传递的struct sockaddr_in 结构体获取ip、port并且返回。那么server.cc以及server.hpp、Dict.hpp中部分内容就需要改变了。Dict.hpp在翻译函数中顺便打印客户端信息,需要多设置一个参数,server.cc的智能指针参数也要改变以及函数体也要改变,server.hpp调用_func的时候多传一个参数
无非就是在lambda表达式传参的时候多一个参数类型,这个参数类型是一个封装的类,server.hpp中的func_t也要多一个参数,这个参数类型的参数的获取是在server.hpp中客户端传来消息中的结构体,这个类的方法在原本的translate中调用,之后调用_func,translate随之被调用
这是代码以及测试结果
InetAddr.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string>
class InetAddr
{
public:
InetAddr(struct sockaddr_in& client):_client(client)
{
_ip = inet_ntoa(_client.sin_addr);
_port = ntohs(_client.sin_port);
}
uint16_t Port()
{
return _port;
}
std::string Ip()
{
return _ip;
}
~InetAddr()
{
}
private:
struct sockaddr_in _client;
uint16_t _port;
std::string _ip;
};
Dict.hpp中变化

server.cc中

server.hpp
#pragma once
#include <iostream>
#include "Log.hpp"
#include "mutexEncapsulation.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <string>
#include <functional>
#include "InetAddr.hpp"
using namespace LogModule;
const int defaultsockfd = -1;
// 声明类型
typedef std::function<std::string(std::string&, InetAddr&)> func_t;
class UdpServer
{
public:
UdpServer(uint16_t port, func_t func)
: _sockfd(defaultsockfd),
_port(port),
_isrunning(false),
_func(func)
{
}
void Init()
{
// 申请套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
logger(LogLevel::FATAL) << "socket error!";
exit(1);
}
logger(LogLevel::INFO) << "socket success, _sockfd : " << _sockfd;
// 绑定ip、端口
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(_port);
addr.sin_addr.s_addr = INADDR_ANY;
int n = bind(_sockfd, (struct sockaddr *)&addr, sizeof(addr));
if (n == -1)
{
logger(LogLevel::FATAL) << "bind error!";
exit(2);
}
logger(LogLevel::INFO) << "bind success, _sockfd : " << _sockfd;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&peer, &len);
if (s > 0)
{
buffer[s] = 0;
InetAddr inetaddr(peer);
std::string word = buffer;
std::string result = _func(word, inetaddr);
ssize_t n = sendto(_sockfd, result.c_str(),
result.size(), 0, (struct sockaddr *)&peer, len);
}
}
}
~UdpServer()
{
}
private:
int _sockfd;
uint16_t _port;
bool _isrunning;
func_t _func;
};

三、chat_UdpServer
v3版本

这是一个聊天的实现,客户端发送消息之后所有的客户端包括自己都应该看见消息的内容。封装一个Route路由类,server收到消息之后将此消息发送到路由通过路由转发给所有的客户端。基本思路和Dict版本一样,lambda传递的函数是Route类里面的MessageRoute方法,Route有一个数组保存所有的客户端InetAddr,首次发送消息就会保存在vector里面,MessageRoute回遍历这个数组并且转发。
MessageRoute在server.hpp中收到消息的时候调用,功能是转发,那就需要使用到sendto接口。所以需要有三个参数:sockfd,message,InetAddr& peer 通过sockfd将message发送给peer
为了方便查看是此时操作的是哪一个在线用户,可以在InetAddr类中疯传一个函数,这个函数将这个用户的ip和port通过字符串的形式返回,我们在MessageRoute类中就可以看到是哪个用户
路由消息向所有在线用户转发,因此需要知道每一个用户的struct sockaddr_in 结构体,在InetAddr中封装一个函数,返回类型就是此结构
但是测试的时候多个客户端必须要先发送消息才能看到其他人发送的消息,这是因为client每次在读的时候都先要写,即sendto,而这个接口设置的是阻塞式的,不先发消息就会一直卡在这里。但是想要实现的是即使一个人不发消息,也应该看见其他人的消息,这也符合多人聊天的规则
所以应该这样做:将发消息和读消息设置为单独的一个线程,而client.cc是主线程,主线程负责创建套接字。这样代码跑起来就会第一时间看到其他人的消息
每次新用户进来应该打印消息并且让所有人看到;用户退出也应该要让所有人看到
这是代码
Route.hpp
#pragma once
#include <vector>
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace LogModule;
class Route
{
public:
Route()
{
}
bool IsExist(InetAddr &peer)
{
for (auto &user : _online_user)
{
if (peer == user) // 需要在InetAddr里面重载==
return true;
}
return false;
}
void AddUser(InetAddr &peer)
{
logger(LogLevel::INFO) << "新增一个在线用户:" << peer.StringAddr();
_online_user.push_back(peer);
}
void DeleteUser(InetAddr &peer)
{
for (auto iter = _online_user.begin(); iter != _online_user.end(); iter++)
{
if (*iter == peer)
{
logger(LogLevel::INFO) << "删除在线用户成功:" << peer.StringAddr();
_online_user.erase(iter);
break;
}
}
}
void MessageRoute(int sockfd, std::string &message, InetAddr &peer)
{
// 不存在这个user就需要添加
if (!IsExist(peer))
{
// 添加的函数也要封装
AddUser(peer);
}
std::string send_message = peer.StringAddr() + message;
// 开始路由消息,向所有在线用户转发
for (auto &user : _online_user)
{
ssize_t n = sendto(sockfd, send_message.c_str(),
send_message.size(), 0, (const struct sockaddr *)&user.SockAddrIn(), sizeof(user.SockAddrIn()));
}
// 若message == "Quit"就表示退出,那么需要在_online_user中删除掉这个用户,封装一个DeleteUser函数
if (message == "Quit")
{
logger(LogLevel::INFO) << "删除一个在线用户";
DeleteUser(peer);
}
}
~Route()
{
}
private:
std::vector<InetAddr> _online_user;
};
server.hpp
#pragma once
#include <iostream>
#include "Log.hpp"
#include "mutexEncapsulation.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <string>
#include <functional>
#include "InetAddr.hpp"
using namespace LogModule;
const int defaultsockfd = -1;
// 声明类型
typedef std::function<void(int, std::string&, InetAddr&)> func_t;
class UdpServer
{
public:
UdpServer(uint16_t port, func_t func)
: _sockfd(defaultsockfd),
_port(port),
_isrunning(false),
_func(func)
{
}
void Init()
{
// 申请套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
logger(LogLevel::FATAL) << "socket error!";
exit(1);
}
logger(LogLevel::INFO) << "socket success, _sockfd : " << _sockfd;
// 绑定ip、端口
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(_port);
addr.sin_addr.s_addr = INADDR_ANY;
int n = bind(_sockfd, (struct sockaddr *)&addr, sizeof(addr));
if (n == -1)
{
logger(LogLevel::FATAL) << "bind error!";
exit(2);
}
logger(LogLevel::INFO) << "bind success, _sockfd : " << _sockfd;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&peer, &len);
if (s > 0)
{
buffer[s] = 0;
InetAddr inetaddr(peer);
std::string message = buffer;
_func(_sockfd, message, inetaddr);
}
}
}
~UdpServer()
{
}
private:
int _sockfd;
uint16_t _port;
bool _isrunning;
func_t _func;
};
server.cc
#include "server.hpp"
#include <memory>
#include "Route.hpp"
// ./server port
int main(int argc, char* argv[])
{
if (argc != 2)
{
std::cerr << "Usage : " << argv[0] << " port" << std::endl;
return 1;
}
uint16_t port = std::stoi(argv[1]);
Enable_ConsoleLog_Strategy();
Route route;
std::unique_ptr<UdpServer> usevr = std::make_unique<UdpServer>(port,
[&route](int sockfd, std::string& message, InetAddr& cli){
route.MessageRoute(sockfd, message, cli);
});
usevr->Init();
usevr->Start();
return 0;
}
client.cc
#include <iostream>
#include "Log.hpp"
#include "mutexEncapsulation.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <string>
#include <unistd.h>
#include "thread.hpp"
using namespace LogModule;
using namespace ThreadModule;
// 定义成全局无需传参
int sockfd;
std::string ip;
uint16_t port;
pthread_t id;
void Send()
{
struct sockaddr_in serv;
memset(&serv, 0, sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_addr.s_addr = inet_addr(ip.c_str());
serv.sin_port = htons(port);
// 第一次发送消息就要告诉所有人新用户来了
std::string input = "inline";
sendto(sockfd, input.c_str(), input.size(), 0,
(struct sockaddr *)&serv, sizeof(serv));
sleep(1);
// 发消息
while (true)
{
std::string input;
std::cout << "please input# ";
std::getline(std::cin, input);
sendto(sockfd, input.c_str(), input.size(), 0,
(struct sockaddr *)&serv, sizeof(serv));
// 当输入是Quit的时候,recev线程也要退出,这里手动退出
if (input == "Quit")
{
pthread_cancel(id);
break;
}
}
}
void Recev()
{
while (true)
{
// 收消息
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
char buffer[1024];
int s = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&peer, &len);
if (s > 0)
{
// 输出server的响应消息
buffer[s] = 0;
std::cerr << buffer << std::endl;
std::cerr.flush();
}
}
}
// ./client serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage" << argv[0] << "ip port" << std::endl;
return 1;
}
ip = argv[1];
port = std::stoi(argv[2]);
// 申请套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1)
{
logger(LogLevel::FATAL) << "socket error!";
return 2;
}
// 不需要绑定
// 创建两个线程,一个send一个recv
Thread send(Send);
Thread recev(Recev);
send.Start();
recev.Start();
id = recev.Id();
send.Join();
recev.Join();
}

但是消息的打印会揉在一起,就是输出和输入混杂,为了方便聊天,要将输出和输入分别显示到不同终端,这里需要使用到重定向操作
将client的输出打印到cerr标准错误,输入输出独立于两个不同终端
用echo ‘num’>/dev/pts确定终端的编号

那么将client的收到消息的输出输出到1,而0则是用来input的;使用指令./client 127.0.0.1 8080 2>/dev/pts/1 (表示将client运行起来的标准错误打印到1终端,而程序的标准错误打印只有recev函数,也就是收到消息的处理)
测试

这样一来,多人聊天的时候自己界面的输出和输入就分开了,并且可以看到来自于不同用户的输入
消息的转发应该设计成多线程的,那么就要引入线程池,每次要广播消息,就像这个任务入线程池,让开辟出来的线程去进行消息的转发,也就是调用MessageRoute。整体上来看,client发送消息到server,server将广播的任务入线程池,线程池调用MessageRoute将消息广播传回给所有的client
但是这个代码还是存在一定问题,当多个client一起聊天的时候,很多消息会进入Route,有的client可能是第一次,那么就要push,有的可能Quit,就要delete,有的可能会将消息广播,这样一来广播的client就可能不是实时的,所以应该要给MessageRoute这个方法加锁,让client不会出现不一致的问题
这是代码变化


四、补充内容
地址转换函数
字符串转ip地址(in_addr)

in_addr转字符串


网络命令
ping
验证网络连通性

netstat
查看网络状态的重要工具

配合使用的指令 watch -n 1 netstat -nltp 表示每一秒自动刷新查看一次网络状态,往后查看的时候就不需要一直输入netstat指令了
pidof
查看进程的pid,方便对进程进行操作,比如配合kill-9杀死进程,

Windows和Linux进行udp通信
不同系统是肯定可以通信的,所以必然会使用的相同的网络协议,那么Windows和Linux下的网络通信的接口绝大部分相似
#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>
#pragma warning(disable : 4996)
#pragma comment(lib, "ws2_32.lib")
std::string serverip = "8.129.16.214"; // 填写你的云服务器ip
uint16_t serverport = 8080; // 填写你的云服务开放的端⼝号
int main()
{
WSADATA wsd;
WSAStartup(MAKEWORD(2, 2), &wsd);
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport); //?
server.sin_addr.s_addr = inet_addr(serverip.c_str());
SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == SOCKET_ERROR)
{
std::cout << "socker error" << std::endl;
return 1;
}
std::string message;
char buffer[1024];
while (true)
{
std::cout << "Please Enter@ ";
std::getline(std::cin, message);
if (message.empty()) continue;
sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct
sockaddr*)&server, sizeof(server));
struct sockaddr_in temp;
int len = sizeof(temp);
int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp,
&len);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer << std::endl;
}
}
closesocket(sockfd);
WSACleanup();
return 0;
}

501



被折叠的 条评论
为什么被折叠?



