预备知识
UDP协议(User Datagram Protocal用户数据报协议)
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
基本思路
如下是我们设计的一个简单的“聊天室”的大致框架图:
“聊天室”分为两个角色,一个是客户端,即参与聊天的用户,另一个是提供服务的服务端,负责接收来自客户端,对接收到的信息加工处理,显示发送方的ip和端口号,再转发给已经加入服务端所创建的用户列表中的所有用户(即已经在该聊天室的用户)。
服务端设计
重要接口详解
服务端设计只要有以下几个步骤:
//第一步 创建套接字socket
**sockfd=socket (int domain, int type, int protocol) **
1.domain指明使用的协议族,常用有AF_INET 、AF_INET6、AF_UNIX 、AF_ROUTE 2. type指明socket类型 有三种:SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)、 SOCK_RAW(原始类型,允许对底层协议如IP或ICMP进行直接访问,不太常用) 3.*protocol* 通常赋值为0; **--成功返回非负值的socket描述符,失败返回-1**
//第二步 将创建的socket绑定到指定的IP地址和端口上
bind(int sockfd, const struct sockaddr myaddr, socklen_t addrlen)*
**--成功返回0,失败返回1**
PS:
1.uint16_t需要头文件 #include <unistd.h> 2.sockaddr_in在头文件
#include<netinet/in.h>或#include <arpa/inet.h>
中定义
3.bzero函数头文件是string.h(C语言)或 cstring(C++) 4.void bzero(void *s, size_t n); 5.bzero函数将指定内存块的前n个字节设置为0。 6.服务器提供服务的端口一般选择大于1023,因为【0,1023】是系统内定的端口号
服务端核心代码
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unordered_map>
#include "Log.hpp"
Log lg;
enum{
SOCKET_ERR=1,
BIND_ERR
};
uint16_t defaultport=8080;
std::string defaultip="0.0.0.0";
const int size=1024;
class UdpServer
{
public:
UdpServer(const uint16_t& port=defaultport,const std::string&ip=defaultip)
:_sockfd(0),
_port(port),
_ip(ip)
{}
//初始化
void Init()
{
//1.创建 udp socket
//udp的socket是全双工的,允许被同时读写
//AF_INET表示使用IPv4地址族 SOCK_DGRAM表示创建一个数据报套接字,0表示以阻塞的方式
_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);
//2.bind socket
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(_port);//需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的
local.sin_addr.s_addr=inet_addr(_ip.c_str()); //string->uint32_t必须是网络序列
if(bind(_sockfd,(const struct sockaddr*)&local,sizeof(local))<0)
{
//绑定失败
lg(Fatal,"bind error,errno:%d,err string:%s",errno,strerror(errno));
exit(BIND_ERR);
}
//绑定成功
lg(Info,"bind success,errno:%d,err string:%s",errno,strerror(errno));
}
void CheckUser(const struct sockaddr_in& client,const std::string clientip,uint16_t clientport)
{
auto iter=_online_user.find(clientip);
if(iter==_online_user.end())
{
_online_user.insert({clientip,client});
std::cout<<"["<<clientip<<":"<<clientport<<"] add to online user."<<std::endl;
}
}
//对存在用户列表的所有用户进行转发
void Broadcast(const std::string&info,const std::string clientip,uint16_t clientport)
{
for(const auto& user:_online_user)
{
std::string message="[";
message += clientip;
message += ":";
message += std::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 n=recvfrom(_sockfd,inbuffer,sizeof(inbuffer)-1,0,(struct sockaddr*)&client,&len);
if(n<0)
{
//未收到
lg(Warning,"recvfrom error,errno:%d,err string:%s",errno,strerror(errno));
continue;
}
uint16_t clientport=ntohs(client.sin_port);
std::string clientip=inet_ntoa(client.sin_addr);//网络字节序列转换 string
//检查该用户是否已在聊天室
CheckUser(client,clientip,clientport);
std::string info=inbuffer;
//向其他成员转发
Broadcast(info,clientip,clientport);
}
}
private:
int _sockfd;
uint16_t _port;
std::string _ip;
bool isrunning; //服务器是否开始运行
std::unordered_map<std::string,struct sockaddr_in> _online_user; //用户列表
};
服务端运行代码
#include "UdpServer.hpp"
#include <memory>
#include <cstdio>
#include <vector>
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}
// ./udpserver port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<UdpServer> svr(new UdpServer(port));
svr->Init(/**/);
svr->Run();
return 0;
}
客户端设计
客户端也是需要绑定端口的,但是不需要用户显示绑定,一般由os自由随机选择,在首次发消息的时候绑定。不同于服务端的是:服务端端口号必须是唯一确定的,客户端可变。
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Terminal.hpp"
using namespace std;
struct ThreadDate
{
struct sockaddr_in server;
int sockfd;
std::string serverip;
};
void Usage(std::string proc)
{
std::cout<<"\n\rUsage:"<<proc<<"serverip serverport\n"<<std::endl;
}
//收信息
void* recv_message(void* args)
{
OpenTerminal();
ThreadDate* td = static_cast<ThreadDate*>(args);
char buffer[1024];
while(true)
{
memset(buffer,0,sizeof(buffer));
struct sockaddr_in tmp;
socklen_t len=sizeof(tmp);
ssize_t n=recvfrom(td->sockfd,buffer,1023,0,(struct sockaddr*)&tmp,&len);
if(n>0)
{
buffer[n]='\0';
cerr<<buffer<<endl;
}
}
}
//发信息
void* send_message(void* args)
{
ThreadDate* td=static_cast<ThreadDate*>(args);
string message;
socklen_t len=sizeof(td->server);
string welcome = td->serverip;
welcome += "coming...";
sendto(td->sockfd,message.c_str(),message.size(),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);
}
}
//多线程
//./udpclient serverip serverporta
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]);
struct ThreadDate td;
bzero(&td.server,sizeof(td.server));
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);
if(td.sockfd<0)
{
cout<<"scoket error"<<endl;
return 1;
}
td.serverip=serverip;
pthread_t recver,sender;
pthread_create(&recver,nullptr,recv_message,&td);
pthread_create(&sender,nullptr,send_message,&td);
pthread_join(recver,nullptr);
pthread_join(sender,nullptr);
close(td.sockfd);
return 0;
}
上述客户端为了用户交互友好,我们打开两个终端模拟,一个终端负责发信息,一个终端负责收信息显示,我们重定向客户端收到消息后,往第二个终端打印。
ls -l /dev/pts //查看我们有哪些终端文件,显示它们的详细信息
例如:
重定向输出信息
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//原来的终端
std::string terminal = "/dev/pts/2";
int OpenTerminal()
{
int fd = open(terminal.c_str(), O_WRONLY);
if(fd < 0)
{
std::cerr << "open terminal error" << std::endl;
return 1;
}
修改到要显示的终端
dup2(fd, 0);
// printf("hello world\n");
// close(fd);
return 0;
}
版权归原作者 lzb_kkk 所有, 如有侵权,请联系我们删除。