服务端server.hpp
构造函数:
cpp
int sockfd_; //网路文件描述符
string ip_; //地址bind 0
uint16_t port_; //表明服务器进程的端口号
bool isrunning_;
unordered_map<string,struct sockaddr_in> online_user_;
网路文件描述符也就就是socket关键字的返回值,是一个文件描述符。
地址ip设置为0.0.0.0,表示监听所有网卡,一台电脑或服务器通常会有多个网络接口(NIC)。比如:回环地址:127.0.0.1,仅本机内部通信使用。以太网:连接局域网的有线IP。无线网卡 :连接Wi-Fi的无线IP。绑定 0.0.0.0 时,程序就会在所有这些网卡上监听端口。这样,外部设备(如同一局域网下的手机)和本机程序都能通过对应的IP地址访问你的服务。
服务端进程的端口号,0~1023一般为应用层协议的端口号,避免冲突,尽量取1024往上。
使用bool变量来确定是否开启动服务器。
unordered_map来存储建立用户的ip和套接字结构体的关联,主要用于服务端收到一个客户端的信息需要sendto给所有客户端,需要使用到客户端的socketaddr_in。
init初始化参数
使用socket创建套接字,第一个参数指定使用IPV4网络协议,第二个参数设置为SOCK_DGRAM,表示指定套接字类型为UDP。在操作系统中打开了一个文件描述符,如果返回值小于0,创建失败。
创建socketaddr_in结构体,sin_family指定IPV4网络协议,sin_port设置监听的端口号,使用htons转换字节序。
sin_addr.s_addr设置监听的IP地址(0.0.0.0)。
使用bind绑定端口和ip,设置进内核中,返回值小于0,说明bind失败。
cpp
void Init()
{
// int socket(int domain(创建套接字的域,使用ipv4的网络协议), int type(套接字的类型), int protocol(协议));
sockfd_=socket(AF_INET,SOCK_DGRAM,0);//
if(sockfd_<0)
{
lg(Fatal,"socket create error,sockfd: %d",sockfd_);
exit(SOCKET_ERR);
}
lg(Info,"socket create success,sockfd: %d",sockfd_);
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(port_);//htons把字节序调整为网路字节序,端口号是要发给对方的。
local.sin_addr.s_addr=inet_addr(ip_.c_str());//inet_addr把字符串转为整数并且转为网络字节序
// int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
int n=bind(sockfd_,(const sockaddr*)&local,sizeof(local));//bind函数在通信时使用一个固定的ip地址和端口号
if(n<0)
{
lg(Fatal, "bind error,error:%d, err string: %s",errno,strerror(errno));
exit(BIND_ERR);
}
lg(Info, "bind success");
}
CheckUser检查用户是否为新用户
如果当前发生数据的用户,没有存储在unordered_map中,说明为新用户,需要添加到哈希表中。
cpp
void CheckUser(const struct sockaddr_in&client,const string clientip,uint16_t clientport)
{
auto iter=online_user_.find(clientip);
if(iter==online_user_.end())
{
online_user_.insert({clientip,client});
cout<<"["<<clientip<<":"<<clientport<<"] add to online user."<<endl;
}
}
Broadcast广播给哈希表中的所有用户
设置用户的ip和端口为string类型,将用户发送过来的数据发送给哈希表里面存储的所有用户。
cpp
#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);
sendto发送成功返回值表示实际发送的字数。发送失败返回值为-1,并设置errno
参数1:套接字文件描述符
参数2:指向要发送数据的缓冲区指针
参数3:要发送数据的字节数
参数4:调用标志位,一般设为0
参数5:输出参数,指向目标地址结构体的指针,会填充对方IP和端口
参数6:输入输出参数,输入:sockaddr结构体的初始长度,输出:实际写入的地址结构体长度
cpp
void Broadcast(const string&info,const string clientip,uint16_t clientport)
{
for(const auto&user:online_user_)
{
string message="[";
message+=clientip;
message+=".";
message+=to_string(clientport);
message+="]#";
message+=info;
socklen_t len=sizeof(user.second);
sendto(sockfd_,message.c_str(),message.size(),0,(struct sockaddr*)(&user.second),len);
}
}
Run接受用户发送来的信息,然后转发
in_addr装字符串函数
cpp
char *inet_ntoa(struct in_addr inaddr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
字符串转in_addr函数
cpp
#include<arpa/inet.h>
int inet_aton(const char*strptr, struct in_addr*addrptr);
in_addr_t inet_addr(const char*strptr);
int inet_pton(int family, const char*strptr, void*addrptr);
cpp
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
recvfrom返回值
接受成功:返回实际发送的字节数
接收失败:返回 -1 ,并设置 errno (如 EAGAIN / EWOULDBLOCK 表示缓冲满, EINTR 表示被信号中断)
inbuffer用户存储接收到客户端发送的信息,使用ntohs和inet_ntoa函数将网络字节序转化为主机字节序。
使用CheckUser检测是否为新用户,及那个inbuffer里面的数据转发给所有的客户端。
cpp
void Run()//简单做一个聊天程序
{
isrunning_=true;
char inbuffer[size];
while(isrunning_)
{
struct sockaddr_in client;
socklen_t len=sizeof(client);
// ssize_t recvfrom(int sockfd, void *buf(接收收到的数据), size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t n=recvfrom(sockfd_,inbuffer,sizeof(inbuffer)-1,0,(struct sockaddr*)&client,&len);
if(n<0)
{
lg(Warning,"recvfrom error: %d , err string :%s",errno,strerror(errno));
continue;
}
uint16_t clientport=ntohs(client.sin_port);
string clientip=inet_ntoa(client.sin_addr);
CheckUser(client,clientip,clientport);//检查是否为新用户
string info=inbuffer;
Broadcast(info,clientip,clientport);
}
}
main.cpp启动服务端
cpp
#include"serve.hpp"
#include<memory>
#include<stdio.h>
void Usage(string proc)
{
cout<<"\n\rUsage: "<<proc<<" port[1024+]\n"<<endl;
}
int main(int argc,char*argv[])
{
if(argc!=2)
{
Usage(argv[1]);
exit(0);
}
uint16_t port=stoi(argv[1]);
unique_ptr<Udpserve> svr(new Udpserve(port));
svr->Init();
// svr->Run(ExcuteCommand);
svr->Run();
return 0;
}
必须包含服务器头文件 UdpServer.hpp 。服务端与客户端是两个独立进程,可同主机,可跨主机。服务端构造函数需要 IP + port。IP 已经默认设为 0.0.0.0 ,用户不需要传。所以用户只需要传入端口号 port,通过命令行参数传递。
使用方式: ./udpserver 8080 。
判断 argc != 2 时打印使用提示 Usage ,高数用户应该怎么输入才可以启动服务端,然后退出。
端口号在 argv[1] ,是字符串,必须用 stoi 转成整数。端口号不需要转网络字节序,交给服务器类处理。使用 unique_ptr 创建服务器对象,自动释放,不需要手动 delete。调用 Init() 完成服务器初始化(创建 socket、bind)。编写业务回调函数 Handler :接收字符串,加工后返回字符串。调用 Run(Handler) ,服务器启动,收到消息自动回调处理。
客户端client.cpp
前置知识:
客户端是不需要手动去bind绑定端口的,操作系统会自动绑定端口上去。
这么做的好处:
1.如果让用户自己指定端口:
王者绑定了 5555,抖音再想绑 5555 就会失败,就会导致应用打不开。把端口交给操作系统随机分配,系统能保证端口不冲突,所有应用都能正常启动。
2.防止恶意程序占满端口
如果允许客户端手动绑定端口:恶意程序一启动就占满所有端口 → 其他应用全部无法联网。交给操作系统管理后,系统会限制进程端口使用,保证其他程序正常运行。
那么系统究竟是什么时候进行的绑定bind的呢?其实是当客户端进程运行起来,首次sendto发送数据的时候进行的绑定,当绑定成功后,此后这个客户端进程运行的生命周期内都不需要再进行绑定了
所以我们可以得出客户端进程需要绑定IP地址和端口号,只不过不需要客户端显示绑定而是由操作系统隐式绑定。
main.cpp
客户端本身并不知道该与哪台主机进行连接,也不清楚要连接主机上哪一个具体的服务端进程,这些信息只有使用程序的用户才清楚。因此我们设计客户端程序时,希望用户通过命令行的方式启动,格式为 ./udpclient 113.46.212.34 6666 ,这就要求客户端的main函数必须携带参数来接收输入。
首先我们要对命令行参数进行判断,使用if语句检查参数个数argc是否等于3。如果不满足,说明用户的传参方式错误,此时需要打印提示信息,告诉用户正确的使用格式,也就是输出Usage提示: ./udpclient IP地址 端口号 。因为参数错误会导致客户端无法获取服务端的IP和端口,程序无法正常启动,所以直接调用exit函数终止进程即可。
当参数校验通过后,就说明用户按照正确格式启动了客户端。此时服务端的IP地址存储在argv[1]中,这是一个点分十进制的字符串格式;端口号存储在argv[2]中,是字符串类型,我们需要用stoi函数将其转换成整型。需要注意的是,此时的端口号只是主机字节序,后续还需要转换为网络字节序才能用于网络通信。
客户端向服务端发送数据需要调用sendto函数,因此我们需要定义一个 struct sockaddr_in 类型的结构体变量server,用于存放服务端的地址信息。首先使用bzero函数对该结构体进行初始化清空,接着按照固定格式填充字段:协议家族设置为UDP对应的AF_INET;将点分字符串IP通过c_str转为C风格字符串,再用inet_addr函数转换为网络字节序的整数;端口号则使用htons函数将主机字节序转换为网络字节序。最后计算出该结构体的长度len,为后续的数据收发做好准备。
cpp
int main(int argc,char*argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(0);
}
string serverip=argv[1];
uint16_t serverport=stoi(argv[2]);
struct ThreadData td;
td.serverip=serverip;
td.server.sin_family=AF_INET;
td.server.sin_port=htons(serverport);
td.server.sin_addr.s_addr=inet_addr(serverip.c_str());
td.sockfd=socket(AF_INET,SOCK_DGRAM,0);
pthread_t recvr,sender;
pthread_create(&recvr,nullptr,recv_message,&td);
pthread_create(&sender,nullptr,send_message,&td);
pthread_join(recvr,nullptr);
pthread_join(sender,nullptr);
close(td.sockfd);
return 0;
}
recv_message接收数据
定义一个buffer数据原来接收数据,调用recvfrom从指定的网络文件描述符中获取数据,将服务端信息设置struct sockaddr_in结构体,在末尾加上\0,变成合法的C风格字符,这边使用cerr答应收到的信息,是为了到时候通过重定向到其他终端来完成输入和收到的信息分开。
cpp
void*recv_message(void*args)
{
ThreadData*td=static_cast<ThreadData*>(args);
char buffer[1024];
while(true)
{
memset(buffer,0,sizeof(buffer));
struct sockaddr_in temp;
socklen_t len=sizeof(temp);
ssize_t s=recvfrom(td->sockfd,buffer,1023,0,(struct sockaddr*)(&temp),&len);
if(s>0)
{
buffer[s]=0;
cerr<<buffer<<endl;
}
}
}
send_message发送数据
第一次发送数据的时候,发送welcome字符串(存储自己的信息)给服务端,之后循环的获取用户输入数据,发送给服务端。
cpp
void*send_message(void*args)
{
ThreadData*td=static_cast<ThreadData*>(args);
string message;
socklen_t len=sizeof(td->server);
string welcome=td->serverip;
welcome+="come...";
sendto(td->sockfd,welcome.c_str(),welcome.size()-1,0,(struct sockaddr*)&(td->server),len);
while(true)
{
cout<<"Please Enter@ ";
getline(cin,message);
sendto(td->sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&(td->server),len);
}
}
测试:
cpp
ls -l /dev/pts
可以查看当前打开了几个终端。


如何确定打开的终端对应哪一个数字呢?

可以echo打印重定向到终端号上面,来确定终端对应的终端号,右上角为5号,右下角为6号。

启动服务器,启动客户端将客户端的err重定向到右上角的终端,这样子,客户端收来的信息就会在右上角出现,右下角就专门给用户输入信息了。

源码:
server.hpp
cpp
#pragma once
#include"log.hpp"
#include<iostream>
#include<string>
#include <sys/types.h>
#include <sys/socket.h>
#include<netinet/in.h>
#include<functional>
#include<arpa/inet.h>
#include <algorithm>
#include<unordered_map>
using namespace std;
uint16_t defaultport=6666; //进程的端口号,0~1023一般为应用层协议的端口号,避免冲突,尽量取1024往上
string defaultip="0.0.0.0"; //一台主机存在的网卡可能不止有一个,对于的ip地址也就不知一个,设为0.0.0.0,只要客户端发送的ip地址是服务端就会被接收
const int size=1024;
enum
{
SOCKET_ERR=1,
BIND_ERR
};
typedef function<string(const string&)> func_t;
Log lg;
class Udpserve
{
public:
Udpserve(const uint16_t&port=defaultport,const string&ip=defaultip)
:port_(port)
,ip_(ip)
,sockfd_(0)
,isrunning_(false)
{}
void Init()
{
// int socket(int domain(创建套接字的域,使用ipv4的网络协议), int type(套接字的类型), int protocol(协议));
sockfd_=socket(AF_INET,SOCK_DGRAM,0);//
if(sockfd_<0)
{
lg(Fatal,"socket create error,sockfd: %d",sockfd_);
exit(SOCKET_ERR);
}
lg(Info,"socket create success,sockfd: %d",sockfd_);
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(port_);//htons把字节序调整为网路字节序,端口号是要发给对方的。
local.sin_addr.s_addr=inet_addr(ip_.c_str());//inet_addr把字符串转为整数并且转为网络字节序
// int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
int n=bind(sockfd_,(const sockaddr*)&local,sizeof(local));//bind函数在通信时使用一个固定的ip地址和端口号
if(n<0)
{
lg(Fatal, "bind error,error:%d, err string: %s",errno,strerror(errno));
exit(BIND_ERR);
}
lg(Info, "bind success");
}
void CheckUser(const struct sockaddr_in&client,const string clientip,uint16_t clientport)
{
auto iter=online_user_.find(clientip);
if(iter==online_user_.end())
{
online_user_.insert({clientip,client});
cout<<"["<<clientip<<":"<<clientport<<"] add to online user."<<endl;
}
}
void Broadcast(const string&info,const string clientip,uint16_t clientport)
{
for(const auto&user:online_user_)
{
string message="[";
message+=clientip;
message+=".";
message+=to_string(clientport);
message+="]#";
message+=info;
socklen_t len=sizeof(user.second);
sendto(sockfd_,message.c_str(),message.size(),0,(struct sockaddr*)(&user.second),len);
}
}
void Run()//简单做一个聊天程序
{
isrunning_=true;
char inbuffer[size];
while(isrunning_)
{
struct sockaddr_in client;
socklen_t len=sizeof(client);
// ssize_t recvfrom(int sockfd, void *buf(接收收到的数据), size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t n=recvfrom(sockfd_,inbuffer,sizeof(inbuffer)-1,0,(struct sockaddr*)&client,&len);
if(n<0)
{
lg(Warning,"recvfrom error: %d , err string :%s",errno,strerror(errno));
continue;
}
uint16_t clientport=ntohs(client.sin_port);
string clientip=inet_ntoa(client.sin_addr);
CheckUser(client,clientip,clientport);//检查是否为新用户
inbuffer[n]=0;
string info=inbuffer;
Broadcast(info,clientip,clientport);
}
}
~Udpserve()
{
if(sockfd_>0)
close(sockfd_);
}
private:
int sockfd_; //网路文件描述符
string ip_; //地址bind 0
uint16_t port_; //表明服务器进程的端口号
bool isrunning_;
unordered_map<string,struct sockaddr_in> online_user_;
};
main.cpp
cpp
#include"serve.hpp"
#include<memory>
#include<stdio.h>
void Usage(string proc)
{
cout<<"\n\rUsage: "<<proc<<" port[1024+]\n"<<endl;
}
int main(int argc,char*argv[])
{
if(argc!=2)
{
Usage(argv[1]);
exit(0);
}
uint16_t port=stoi(argv[1]);
unique_ptr<Udpserve> svr(new Udpserve(port));
svr->Init();
// svr->Run(ExcuteCommand);
svr->Run();
return 0;
}
client.cpp
cpp
#include<iostream>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <pthread.h>
using namespace std;
struct ThreadData
{
struct sockaddr_in server;
int sockfd;
string serverip;
};
void Usage(string proc)
{
cout<<"\n\rUsage: "<<proc<<"sercerip serverproc\n"<<endl;
}
void*recv_message(void*args)
{
ThreadData*td=static_cast<ThreadData*>(args);
char buffer[1024];
while(true)
{
memset(buffer,0,sizeof(buffer));
struct sockaddr_in temp;
socklen_t len=sizeof(temp);
ssize_t s=recvfrom(td->sockfd,buffer,1023,0,(struct sockaddr*)(&temp),&len);
if(s>0)
{
buffer[s]=0;
cerr<<buffer<<endl;
}
}
}
void*send_message(void*args)
{
ThreadData*td=static_cast<ThreadData*>(args);
string message;
socklen_t len=sizeof(td->server);
string welcome=td->serverip;
welcome+="come...";
sendto(td->sockfd,welcome.c_str(),welcome.size()-1,0,(struct sockaddr*)&(td->server),len);
while(true)
{
cout<<"Please Enter@ ";
getline(cin,message);
sendto(td->sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&(td->server),len);
}
}
int main(int argc,char*argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(0);
}
string serverip=argv[1];
uint16_t serverport=stoi(argv[2]);
struct ThreadData td;
td.serverip=serverip;
td.server.sin_family=AF_INET;
td.server.sin_port=htons(serverport);
td.server.sin_addr.s_addr=inet_addr(serverip.c_str());
td.sockfd=socket(AF_INET,SOCK_DGRAM,0);
pthread_t recvr,sender;
pthread_create(&recvr,nullptr,recv_message,&td);
pthread_create(&sender,nullptr,send_message,&td);
pthread_join(recvr,nullptr);
pthread_join(sender,nullptr);
close(td.sockfd);
return 0;
}
makefile
cpp
.PHONY:all
all:server client
server:Main.cpp
g++ -o $@ $^ -std=c++11
client:client.cpp
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f server client