当前位置:首页 » 《随便一记》 » 正文

【Linux】基于UDP协议的“聊天室”

17 人参与  2024年02月12日 08:01  分类 : 《随便一记》  评论

点击全文阅读


目录

预备知识

基本思路

服务端设计

重要接口详解

服务端核心代码

服务端运行代码

客户端设计


预备知识

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 portint 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 serverportaint 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;}


点击全文阅读


本文链接:http://zhangshiyu.com/post/68200.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1