观前提示:
本文涉及了以前博文实现的相关内容,在此贴出
线程的封装 Thread.hpp 及 日志
Liunx--线程池的实现--0208 09_Gosolo!的博客-CSDN博客
1. 网络编程相关接口
1.1 创建套接字
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain,int type,int protocol);
返回值是一个套接字
参数:
domain 域,创建哪一种类型的套接字 AF_INET 表示是网络通信类型
type 通信种类
protocol 设为0值即可
1.2 绑定套接字
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr*addr,socklen_t addrlen);
成功返回0
sockfd 要绑定的套接字
addr 是sockaddr这个通用的结构体,我们使用网络传输时,就需要创建sockaddr_in结构体,然后将他强转为 sockaddr 结构体。
addrlen 传入的结构体的长度
1.2.1 sockaddr_in结构体
1.3 读数据
#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);
返回读到的字节数,失败为-1
buf 缓冲区
len 缓冲区的大小
flags 给0时,为阻塞方式读取
除了读取发来的数据,也需要知道是谁发来的数据
src_addr addrlen 为输出型参数 方便后续给发来数据的程序回消息
src_addr 结构体的地址
addrlen 结构体的长度
1.4 发送消息
#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);
dest_addr 你要把数据发给谁
addrlen 这个谁的缓冲区有多长
1.5 IP补充及查看网络连接指令
ip地址
127.0.0.1
称为本地环回,client和server发送数据只在本地协议栈中进行数据流动,不会把我们的数据发送到网络中。
注意:一个具体的公网ip在云服务器上无法绑定。
查看网络连接
netstat -anup
1.6 网络字节序
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
h代表host,主机之意。n表示network,网络。l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送
in_addr_t inet_addr(const char *cp);
将string类型转换成整数类型 然后再修改成网络字节序
char *inet_ntoa(struct in_addr in);
将以网络字节序给出的网络主机地址,转换成以点分十进制表示的字符串(如127.0.0.1)。结果作为函数返回结果返回。
2.Upd单线程demo
创建一个服务器,首先需要设置自己的端口号和ip,以便其他人发消息给自己。
2.1 基础框架
2.1.1 服务端的框架
这里私有成员变量还有一个 int _sock; 方便我们后续使用套接字
2.1.2 服务端的运行
按照常理来讲,如果想要运行一个服务器,那就需要确定他的ip地址和端口号,但是由于云服务器的ip地址并不真实的,所以也不建议绑定一个具体的公网ip。至于不传ip如何建立相关性,我们在UpdServer() (服务端的具体构造函数中)再谈
#include <iostream>
#include "udp_server.hpp"
#include <memory>
#include <cstdlib>
static void usage(std::string proc)
{
std::cout<<"\nUsage:"<<proc<<"ip port\n"<<std::endl;
}
//要让服务器运行起来,需要知道 ip地址 和端口号
int main(int argc, char*argv[] )
{
if(argc != 2)
{
usage(argv[0]);
exit(1);
}
// std::string ip = argv[1];
uint16_t port = atoi(argv[1]);
std::unique_ptr<UdpServer> svr(new UdpServer(port));
svr->initServer();
svr->Start();
return 0;
}
2.1.3 客户端的运行
运行时需要带上 ip 和端口号 表示该客户端要像谁发送信息
问题: 那我自己的ip如何绑定呢?我给服务端发消息了 客户端怎么给我发回来呢?
之前提到过 一个进程可以对应多个端口。
client是一个客户端进程,如果显示的绑定了一个确定的端口,会导致固定使用这个端口。万一,其他的客户端提前占用了这个端口呢?
所以,客户端client一般不需要显示的bind指定port,而是让OS自动随机选择。
什么时候做的呢?当客户端首次发送信息给服务器时,OS会自动给客户端bind他的id和port
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
//单线程版本的
static void usage(std::string proc)
{
std::cout<<"\nUsage: "<<proc<<"serverIp serverPort\n"<<std::endl;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
usage(argv[0]);
exit(1);
}
//创建 套接字
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock<0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
std::string message;
struct sockaddr_in server;//网络通信 要申请 sockaddr_in结构体
memset(&server,0,sizeof(server));//初始化这个结构体为0
server.sin_family=AF_INET;
//在结构体里面 给port赋值 首先需要将输入参数的string类型 转换为整形类型
//然后因为数据的传输是双方的 所以需要传入网络 所以需要 htons函数
server.sin_port=htons(atoi(argv[2]));
//接下来需要修改server中的ip 但由于嵌套的结构体封装内 接收ip地址的是一
//个unint32_t类型.所以我们需要先将string类型转换成整数类型 然后再修改
//这个过程有一个方便的接口
server.sin_addr.s_addr=inet_addr(argv[1]);
char buffer[1024];//保存读到的数据
while(true)
{
std::cout<<"请输入你的信息# ";
std::getline(std::cin,message);
if(message=="quit") break;
sendto(sock,message.c_str(),message.size(),0,
(structsockaddr*)&server,sizeof(server));
//发送消息完毕 接下来是接收消息 客户端给我返回来的消息
struct sockaddr_in temp;//拿一个结构体来接收数据
socklen_t len=sizeof(temp);
ssize_t s=recvfrom(sock,buffer,sizeof buffer,0,
(struct sockaddr*)&temp,&len);
if(s > 0)
{
buffer[s] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
close(sock);
return 0;
}
2.2 服务端的实现
2.2.1 构造和析构
上述说了对于服务端而言,不建议绑定确定的公网ip,我们这里直接缺省值给成空串
UdpServer(uint16_t port,std::string ip="")
:_port(port)
,_ip(ip)
{}
~UdpServer()
{
if (_sock >= 0)
close(_sock);
}
2.2.2 服务器的初始化
bool initServer()
{
//初始化我的套接字
_sock=socket(AF_INET,SOCK_DGRAM,0);
if (_sock < 0)
{
logMessage(FATAL, "%d:%s", errno, strerror(errno));
exit(2);
}
//网络通信 需要创建一个 sockaddr_in 结构体
struct sockaddr_in local;
//将该结构体初始化全0 也可以使用meset
//memset(&local,0,sizeof(local));
bzero(&local, sizeof(local));
local.sin_family = AF_INET;//保持和domain即创建套接字的第一个参数 一致即可
/*
将本地端口字节序改成网络字节序
由于构造的时候 传入的port本就是整形类型 所以直接调用 htons函数即可
*/
local.sin_port=htons(_port);
/*
修改本地ip字节序为网络字节序
1.首先 字符串类型转成 整形类型
2. 转成网络字节序
综上 我们使用 inet_addr()接口
记得我们构造时,将ip给的是空串吗?不给一个ip地址是不行的,别人发消息
就收不到了。我们这里使用了INADDR_ANY 这是一个宏 本质是一串0 他的作用
我们后续单独来谈
*/
local.sin_addr.s_addr=_ip.empty()?INADDR_ANY:inet_addr(_ip.c_str());
if (bind(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "%d:%s", errno, strerror(errno));
exit(2);
}
return true;
}
2.2.4 Start()接口
void Start()
{
char buffer[SIZE];
for (;;)
{
// peer,纯输出型参数
struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
socklen_t len = sizeof(peer);
//读取数据
ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1,
0, (struct sockaddr *)&peer, &len);
if(s>0)
{
buffer[s]=0;//数据当成字符串
uint16_t cli_port=ntohs(peer.sin_port);//从网络中来的
/*
将ip由网络字节序转成主机字节序
记得ip地址在sockaddr_in中被封装成了一个结构体
使用 inet_ntoa() 接口
*/
std::string cli_ip=inet_ntoa(peer.sin_addr);
printf("[%s:%d]# %s\n", cli_ip.c_str(),cli_port,buffer);
}
//读取完毕 开始发送
sendto(_sock,buffer,strlen(buffer),0,(struct sockaddr*)&peer,len);
}
}
2.3 测试结果
2.3.1 客户端自动绑定端口
发现 客户端自动bind了一个端口
2.3.2 INADDR_ANY
INADDR_ANY的意义
让服务器在工作过程中 可以从任意位置获取数据 只要端口号符合就可以。
3. udp编程--多线程改进版本
只有服务端可以收到别人发来的全部消息,而客户端彼此看不到客户端的消息。我们针对于这
一点进行改进。
- 首先,在服务端中 我们需要一个数据结构,里面保存了所有客户端发来的消息和他的ip port等信息,可以采用哈希表的方式。
unordered_map<std::string, struct sockaddr_in>
- 其次,按照单线程逻辑,当一方接收到消息时,一定有这样几个过程。1.接收消息 2.处理信息 3.发回消息。由于发送消息需要使用getline()接口,不输入消息就会阻塞在这个地方。后面的接收消息等代码不会被执行。要想收到别人的消息,需要自己先发一条消息,这不合理。
- 如果需要让服务端看到别人的消息,就需要服务端中的读写消息的功能同时进行,即使用多线程帮助。
3.1 多线程版本的服务端的实现
在Udp_Server类中新增一个unorder_map<string,struct sockaddr_in>类型
key作为关键字,提示消息是哪个用户发送的,我们采用的是ip+port构成key
哈希表仅用于客户端向用户端发送消息,没有涉及其他模块,我们只需要修改
Udp_Server::Start()
void Start()
{
char buffer[SIZE];
for (;;)
{
struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
socklen_t len = sizeof(peer);
// 读取数据
ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1,
0, (struct sockaddr *)&peer, &len);
//给一个关键字 用于放入哈希表
char key[64];
if(s>0)
{
buffer[s]=0;//数据当成字符串
uint16_t cli_port=ntohs(peer.sin_port);
std::string cli_ip=inet_ntoa(peer.sin_addr);
//利用ip 和 port构建一个名字 127.0.0.1-8080
snprintf(key, sizeof(key), "%s-%u", cli_ip.c_str(), cli_port);
logMessage(NORMAL, "key: %s", key);
auto it = _users.find(key);
if (it == _users.end())
{
//将他纳入哈希表
logMessage(NORMAL, "add new user : %s", key);
_users.insert({key, peer});
}
}
for (auto &iter : _users)
{
std::string sendMessage = key;
sendMessage += "# ";
sendMessage += buffer; // 127.0.0.1-1234# 你好
logMessage(NORMAL, "push message to %s", iter.first.c_str());
sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0,
(struct sockaddr *)&(iter.second), sizeof(iter.second));
}
}
}
3.2 多线程版本的客户端
引入线程的封装文件,#include "Thread.hpp"
3.2.1 主函数
/*
下面这两个全部变量 可以像ThreadData一样进行进一步封装
这里没有实现
*/
uint16_t serverport = 0;
std::string serverip;
int main(int argc,char* argv[])
{
if(argc!=3)
{
usage(argv[0]);
exit(1);
}
//创建 套接字
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock<0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
serverport = atoi(argv[2]);
serverip = argv[1];
//创建两个线程
std::unique_ptr<Thread> sender(new Thread(1,udpSend,(void*)&sock));
std::unique_ptr<Thread> recver(new Thread(2,udpRecv,(void*)&sock));
sender->start();
recver->start();
sender->join();
recver->join();
close(sock);
return 0;
}
3.2.2 回调函数
static void *udpSend(void *args)
{
int sock = *(int *)((ThreadData *)args)->args_;
std::string name = ((ThreadData *)args)->name_;
std::string message;
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());
while (true)
{
std::cerr << "请输入你的信息# "; //标准错误 2打印
std::getline(std::cin, message);
if (message == "quit")
break;
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);
}
return nullptr;
}
static void* udpRecv(void* args)
{
int sock=*(int*)((ThreadData*)args)->args_;
std::string name=((ThreadData*)args)->name_;
char buffer[1024];
while(true)
{
memset(buffer,0,sizeof(buffer));
struct sockaddr_in temp;
socklen_t len=sizeof(temp);
ssize_t s=recvfrom(sock,buffer,sizeof(buffer),0,(struct sockaddr*)&temp,&len);
if(s>0)
{
buffer[s];
std::cout << buffer << std::endl;
}
}
}
3.3 多线程版本测试结果
如果直接运行程序,客户端会显示自己发送的消息,客户端发回的消息,以及其他人的消息杂成一团,所以我们利用管道文件,将收到的消息重定向到该管道文件
mkfifo clientA
创建管道文件 clientA