Linux 套接字编程---基于UDP协议实现简易的聊天室

服务端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
相关推荐
我爱学习好爱好爱2 小时前
ELK日志分析平台(三):Logstash 7.17.10 独立节点部署与基础测试(基于Rocky Linux 9.6)
linux·python·elk
默|笙2 小时前
【Linux】库制作与原理(2)_ELF格式
linux·运维·服务器
青桔柠薯片2 小时前
Linux I/O多路复用:深入浅出poll与epoll
linux·运维·服务器·算法
雾岛听蓝2 小时前
Linux文件系统:从硬件到软硬链接
linux·经验分享·笔记
HalvmånEver2 小时前
Linux:初始网络(上)
linux·网络·学习·通信
REDcker3 小时前
Linux C++ 内存泄漏排查分析手册
java·linux·c++
切糕师学AI3 小时前
Kubernetes Operator 详解
运维·分布式·云原生·容器·kubernetes·自动化·运维自动化
Hello World . .3 小时前
Linux:网络编程-基于HTTP协议的天气预报查询系统开发详解
linux·网络·http
软件资深者3 小时前
macOS Tahoe 26.3.1 ISO 虚拟机专用镜像:win系统/ESXi 服务器装苹果系统,改个后缀就能用
运维·服务器·macos·镜像·虚拟机