【Linux】udp网络程序

一.端口号

  • 进行网络通信的时候,网络协议中的下三层(传输层、网络层、数据链路层)主要解决的是将数据安全可靠的送到远端机器,用户使用应用层软件完成数据的发送和接受的,使用前需要先把软件启动起来,变为进程,所以网络通信的本质就是进程间通信,进程具有独立性,进行通信前需要看到一份独立的资源,所以两边主机通过贯穿网络协议栈看到网络这份共享资源来进行通信
  • 网络应用层上有很多不同的进程来执行不同的功能,端口号是用来唯一标识主机上一个网络应用层的进程
  • 在公网上,IP地址能表示唯一的一台主机,端口号port,用来标识该主机上的唯一的一个进程,IP+Port就可以标识全网唯一的一个进程
  • 端口号(port)是传输层协议的内容.
    1.端口号是一个2字节16位的整数
    2.端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
    3.IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
    4.一个进程可以绑定多个端口号,但一个端口号只能被一个进程占用
  • pid已经能够标识一台主机上进程的唯一性了,为什么还要搞一个端口号?
    1.不是所有进程都要网络通信,但是所有进程都要pid
    2.实现系统和网络功能的解耦
  • 端口号存在于传输层的报头,传输层的核心查询场景是 "四元组匹配"(源 IP、源端口、目标 IP、目标端口),或 "二元组匹配"(本地 IP + 本地端口),传输层中有哈希表作为辅助索引,会以 "二元组" 或 "四元组" 为哈希键,快速定位到对应的连接结构体(如Linux中的 struct sock)

二.认识TCP协议与UDP协议

  • TCP协议
    传输层协议
    有连接
    可靠传输(会在意丢包问题)
    面向字节流
  • UDP协议
    传输层协议
    无连接
    不可靠传输(把数据发送出去就不管了)
    面向数据报

1.网络字节序

  • 内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,
    网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢

1.发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;

2.接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;

3.因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.

4.TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.

5.不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;

6.如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

  • 为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回 。

如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

2.socket 常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)

int socket(int domain, int type, int protocol);

// 绑定端口号 (TCP/UDP, 服务器)

int bind(int socket, const struct sockaddr address, socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr
address, socklen_t* address_len);

// 建立连接 (TCP, 客户端)

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

  • 套接字编程的种类:
    1.域间套接字编程:
    用于同一主机内进程间通信(IPC)的套接字,不依赖网络协议
    2.原始套接字编程:
    直接操作底层网络协议(如 IP、ICMP)的套接字,可绕过传输层(TCP/UDP)
    3.网络套接字编程:
    基于 TCP/UDP 和 IP 协议的套接字,用于不同主机间的网络通信
  • 接口中常用sockaddr结构
    socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同
  • AF_INET表示地址族为 IPv4 互联网地址族,sockaddr_in结构体用于 IPv4 网络通信 ,适用于不同主机之间通过互联网或局域网进行通信的场景
  • AF_UNIX(也称为AF_LOCAL )表示本地通信地址族,sockaddr_un结构体用于同一台主机上不同进程之间的通信,又被称为 Unix 域套接字。
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

三.UDP网络程序

创建套接字的系统调用socket

1.参数domain(地址族 / 协议族):

决定了通信时使用的地址格式(如 IPv4、IPv6、本地进程间通信等),常见取值有以下:

AF_INET:IPv4 互联网地址族(最常用,用于不同主机通过 IPv4 网络通信)。

AF_INET6:IPv6 互联网地址族。

AF_UNIX(或 AF_LOCAL):本地进程间通信的地址族
2.参数type(套接字类型):

用于指定套接字的通信语义

SOCK_STREAM:流式套接字,基于 TCP 协议,提供可靠、面向连接、字节流的通信(数据无丢失、无重复、按序到达),适合文件传输、网页访问等场景。

SOCK_DGRAM:数据报套接字,基于 UDP 协议,提供不可靠、无连接、数据报的通信(数据可能丢失 / 乱序,但速度快),适合视频通话、实时游戏等场景。

SOCK_RAW:原始套接字,允许直接访问网络层(如 IP 协议),可用于网络诊断、自定义协议等(需要管理员权限)。
3. protocol(协议类型)

用于指定套接字使用的具体协议(当 type 对应多种协议时,需明确指定;否则通常传 0)

若 type 为 SOCK_STREAM 且 domain 为 AF_INET,传 0 会默认选择 TCP 协议。

若 type 为 SOCK_DGRAM 且 domain 为 AF_INET,传 0 会默认选择 UDP 协议。

特殊场景(如原始套接字)需显式指定协议(如 IPPROTO_ICMP 用于 ICMP 协议)。
4.返回值

调用成功时,会返回一个非负整数,这个整数被称为套接字描述符,也叫文件描述符(在 UNIX 和类 UNIX 系统中,套接字在一定程度上被当作文件来处理 ),用于标识新创建的套接字。在后续的操作,比如bind(绑定地址和端口)、connect(连接到服务器)、send(发送数据)、recv(接收数据)等函数调用中,都需要使用这个套接字描述符来指定操作的对象。调用失败时,会返回 -1 ,并且会设置全局变量errno来指示错误的具体原因

sockaddr_in结构

虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址.

实现整数ip与字符串ip的互相转化

套接字中接受数据的系统调用recvfrom

  • recv 函数

作用:用于从已连接的套接字(通常是 TCP 套接字)接收数据。

参数:

sockfd:套接字描述符,标识要接收数据的套接字。

buf:指向用于存储接收数据的缓冲区。

len:缓冲区的长度(字节数)。

flags:接收数据时的选项标志,一般设为 0 即可,也可指定如 MSG_WAITALL(等待接收完整数据)等标志。

返回值:成功时返回接收到的字节数,失败时返回 -1。

  • recvfrom 函数

作用:主要用于从无连接的套接字(通常是 UDP 套接字)接收数据,还能获取发送方的地址信息。

参数:在 recv 函数参数基础上,多了 src_addr(用于存储发送方地址信息的结构体指针)和 addrlen(用于指定地址结构体长度的指针,是输=输入输出型参数)。

返回值:同 recv 函数,成功返回接收到的字节数,失败返回 -1

  • recvmsg 函数

作用:是功能最强大的接收数据函数,支持接收带辅助数据(如控制信息)的消息,能更灵活地处理复杂的消息接收场景。

参数:

sockfd:套接字描述符。

msg:指向 msghdr 结构体的指针,该结构体包含了接收消息的各种信息,如缓冲区、地址信息、辅助数据等。

flags:接收选项标志。

返回值:成功返回接收到的字节数,失败返回 -1

发送数据的系统调用sendto

  • send 函数

用于向已连接的套接字(通常是 TCP 套接字)发送数据

  • sendto 函数
    主要用于向无连接的套接字(通常是 UDP 套接字)发送数据,需要指定接收方的地址信息,addrlen是输入型参数
  • sendmsg 函数
    是功能最强大的发送数据函数,支持发送带辅助数据(如控制信息)的消息,能更灵活地处理复杂的消息发送场景

地址转换函数

sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址,我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换

  • 字符串转in_addr的函数:
  • in_addr转字符串的函数:

    其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。
  • 关于inet_ntoa:
    inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果,man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放。
  • 当多次调用时会出现以下结果


    因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。
    注意:
    多线程调用inet_ntoa在centos7上测试没有问题,内部可能实现了互斥锁,手册中规定它不是线程安全函数。
    在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;

服务端的ip和port绑定问题

1024以后的端口号可以供我们自由使用(除mysql绑定的之外)

除绑定任意地址0外,还可以绑定内网IP或者本地环回地址,IPv4 环回地址标准地址段:127.0.0.0 ~ 127.255.255.255(共 1600 多万个地址),其中最常用的是 127.0.0.1, IPv6 环回唯一地址:::1(等价于 IPv4 的 127.0.0.1),没有地址段,仅这一个地址用于本地环回。

环回地址的规则:

所有属于 127.x.x.x 的地址,都会被操作系统内核直接 "拦截",不会发送到物理网卡(无论网线是否插好、是否有外部网络),数据仅在本地内存中循环。

1.代码实现

UdpServer.hpp

cpp 复制代码
#pragma once

#include<iostream>
#include<string>
#include<strings.h>
#include<cstring>
#include<functional>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include "log.hpp"

using namespace std;
//指定处理客户端请求的回调函数
typedef function<string(const string&)> func_t;

uint16_t defaultport=8888;
string defaultip="0.0.0.0";
const int size=1024;

Log lg;
enum{
    SOCKET_ERR=1,
    BIND_ERR
};

class UdpServer{
public:
    UdpServer(const uint16_t &port=defaultport,const string &ip=defaultip)
    :port_(port),ip_(ip),sockfd_(0),isrunning_(false)
    {}
    void Init()
    {
        //1.创建Udp socket
        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));//非标准c库函数,将指定内存区域前n字节设置为0
       local.sin_family=AF_INET;
       local.sin_port=htons(port_);//保证端口号是网络字节序列,要发送给别人
       local.sin_addr.s_addr=inet_addr(ip_.c_str());//inet_addr将点分十进制格式的 IPv4 地址字符串转换成网络字节序的 32 位整数
       //local.sin_addr.s_addr=htonl(INADDR_ANY);//将套接字绑定到本机所有可用的 IPv4 地址,不限制特定IP,INADDR_ANY:是一个宏定义值为 0
       //为什么是local.sin_addr.s_addr?sin_addr是struct in_addr类型的变量,而s_addr是该结构体中存储IP地址的具体成员
       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 Run(func_t func)//对代码进行分层
    {
        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;
            }
            inbuffer[n]=0;
            string info=inbuffer;
            //调用回调函数处理数据
            string echo_string=func(info);
            //返回数据给客户端
            sendto(sockfd_,echo_string.c_str(),echo_string.size(),0,(const sockaddr*)&client,sizeof(client));
        }
    }
    ~UdpServer()
    {
        if(sockfd_>0) close(sockfd_);
    }
private:
    int sockfd_;//网络文件描述符
    string ip_;//0代表任意地址
    uint16_t port_;//服务进程的端口号
    bool isrunning_;
};

整体思路:

通过创建 UDP 套接字、绑定地址端口,然后在一个循环中不断接收客户端数据、处理数据并返回结果,实现了一个简单的 UDP 服务器,能够根据传入的回调函数对客户端请求进行灵活处理。

UdpClient.cc

cpp 复制代码
#include<iostream>
#include<cstdlib>
#include<unistd.h>
#include<strings.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

using namespace std;

void Usage(string proc)
{
    cout<<"\n\rUsage: "<<proc<<"serverip serverport\n"<<endl;
}

// ./udpclient serverip serverport总共接受三个参数
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 sockaddr_in server;
    bzero(&server,sizeof(server));
    server.sin_family=AF_INET;
    server.sin_port=htons(serverport);//将主机字节序转化为网络字节序
    server.sin_addr.s_addr=inet_addr(serverip.c_str());//将点分十进制的 IP 字符串转换为网络字节序的整数
    socklen_t len=sizeof(server);
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd<0)
    {
        cout<<"socker error"<<endl;
        return 1;
    }

    string message;
    char buffer[1024];
    while(true)
    {
        cout<<"please enter@ ";
        getline(cin,message);//从标准输入读取用户输入的消息
        //将消息发送到服务器的地址(server结构体)
        sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,len);

        struct sockaddr_in temp;
        socklen_t len=sizeof(temp);
        //接收服务器返回的响应数据
        ssize_t s=recvfrom(sockfd,buffer,1023,0,(struct sockaddr*)&temp,&len);
        if(s>0)
        {
            buffer[s]=0;
            cout<<buffer<<endl;
        }
    }
    close(sockfd);
    return 0;
}

整体思路:

客户端通过 UDP 套接字,不断向服务器发送用户输入的消息,并接收服务器的响应,实现 "发送 - 接收" 的交互流程,属于典型的 UDP 客户端通信模型。

  • 注意:

client与server不同,不需要显示调用bind函数绑定端口号,一个端口号只能被一个进程绑定,对server和client来说都是这样,所以client端口号是多少不重要,只要能保证主机上的唯一性就行。在输入指令时带上服务端的port操作系统就会自动绑定,是在首次发送信息时绑定。

实际场景理解:

日常生活中使用各大厂商的app,在自己的设备上相当于一个个客户端,如果每个app都自己绑定了一个端口号,万一其他厂家绑定一样的那么就会造成冲突,若要固定绑定端口号沟通成本比较大,也没必要,所以设定为由操作系统自动绑定,只要能唯一标识一个客户端即可。

  • 服务器必须固定端口,因为客户端需要知道向哪个端口发送请求,客户端不需要固定端口因为需求是向服务器请求服务,没必要让其提前知道自己的端口

main.cc

cpp 复制代码
#include"UdpServer.hpp"
#include<memory>
#include<cstdio>

void Usage(string proc)
{
    cout<<"\n\rUsage: "<<proc<<" port[1024+]\n"<<endl;
}
//提供两个可选的客户端消息处理函数,供 Run 方法调用:
string Handler(const string&str)
{
    string res="server get a message: ";
    res+=str;
    cout<<res<<endl;
    return res;
}

string ExcuteCommand(const string &cmd)
{//执行系统命令(将客户端消息作为命令),通过 popen 创建管道读取命令输出,
//将输出结果返回给客户端(若命令不存在会返回错误信息)
    FILE*fp=popen(cmd.c_str(),"r");//创建管道并以只读方式打开
    if(nullptr==fp)
    {
        perror("popen");
        return "error";
    }
    string result;
    char buffer[4096];
    while(true)
    {
        char*ok=fgets(buffer,sizeof(buffer),fp);//从管道读取数据到buffer
        if(ok==nullptr) break;
        result+=buffer;
    }
    pclose(fp);
    return result;
}
// ./udpserver port
int main(int argc,char*argv[])
{
    if(argc!=2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port=stoi(argv[1]);
    //使用智能指针 unique_ptr 管理 UdpServer 实例,
    //避免手动释放资源的风险:通过 new UdpServer(port) 传入端口号创建实例。
    unique_ptr<UdpServer> svr(new UdpServer(port));
    svr->Init();
    svr->Run(Handler);//启动服务器
    return 0;
}

整体思路:

通过 UdpServer 类封装服务器核心能力(初始化、循环处理),主程序只需负责参数解析、创建实例和指定处理逻辑,实现 "业务逻辑与网络通信解耦",既灵活又便于维护(如需切换处理逻辑,只需将 Run 的参数换成 ExcuteCommand 即可)

  • log.hpp复用<进程间通信文章>的代码
    makefile如下
cpp 复制代码
.PHONY:all
all:udpserver udpclient

udpserver:main.cc
	g++ -o $@ $^ -std=c++11
udpclient:UdpClient.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f udpserver udpclient

netstat -nlup(naup)指令可以查看网络状态,端口号最多可以绑到2的16次方

-tlp后缀可以查看22号进程,可以使用xshell的原因,xshell相当于一个客户端将你的指令打包发给网络,由服务器接收处理后再打包返回

2.多线程版本

  • 关于终端号
  • 可以通过多个终端来实现udp服务端、客户端的信息发送端与接收端的分离。注意这个0、1终端号和之前学的文件描述符号有点像但不是一个东西,伪终端设备编号主要服务于系统对终端会话的管理以及用户与终端的交互;而文件描述符聚焦于进程内部对输入输出和文件的操作。
  • 用ls -l/dev/pts/可以查看终端,打开多个终端时,通过echo语句向/dev/pts/不同终端号打印,看在哪个终端上显示。也可以直接用命令行做重定向
  • 自己写的程序是可以朝自己的终端上打印的
cpp 复制代码
#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<cstdio>
using namespace std;
string terminal="/dev/pts/2";
int main()
{
    int fd=open(terminal.c_str(),O_WRONLY);
    if(fd<0)
    {
        cerr<<"open terminal error"<<endl;
        return 1;
    }
    dup2(fd,1);//将标准输出重定向到伪终端当中
    printf("hello world\n");
    close(fd);
    return 0;
}

代码实现

  • UdpServer.hpp
cpp 复制代码
#pragma once

#include<iostream>
#include<string>
#include<strings.h>
#include<cstring>
#include<functional>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include "log.hpp"
#include<unordered_map>

using namespace std;
//指定处理客户端请求的回调函数
//typedef function<string(const string&)> func_t;
typedef function<string(const string&,const string&,uint16_t)> func_t;


uint16_t defaultport=8888;
string defaultip="0.0.0.0";
const int size=1024;

Log lg;
enum{
    SOCKET_ERR=1,
    BIND_ERR
};

class UdpServer{
public:
    UdpServer(const uint16_t &port=defaultport,const string &ip=defaultip)
    :port_(port),ip_(ip),sockfd_(0),isrunning_(false)
    {}
    void Init()
    {
        //1.创建Udp socket
        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));//非标准c库函数,将指定内存区域前n字节设置为0
       local.sin_family=AF_INET;
       local.sin_port=htons(port_);//保证端口号是网络字节序列,要发送给别人
       local.sin_addr.s_addr=inet_addr(ip_.c_str());//inet_addr将点分十进制格式的 IPv4 地址字符串转换成网络字节序的 32 位整数
       //local.sin_addr.s_addr=htonl(INADDR_ANY);//将套接字绑定到本机所有可用的 IPv4 地址,不限制特定IP,INADDR_ANY:是一个宏定义值为 0
       //为什么是local.sin_addr.s_addr?sin_addr是struct in_addr类型的变量,而s_addr是该结构体中存储IP地址的具体成员
       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 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(auto &user:online_user_)
        {
            string message="[";
            message+=clientip;
            message+=":";
            message+=to_string(clientport);
            message+="]# ";
            message+=info;
            socklen_t len=sizeof(user.second);
            std::cout << "message: " << message << std::endl;//这里的message的初始大小为添加的信息
            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;
            }
            //接收的字符串不会自动添加\0,数组本身都是\0,接收的第一个字符串比如是  1234\0\0...  
            //第二次,接收的数据是 66,此时数组内容就是  6634\0\0 ,若不手动置0就会有这样的打印错误
            inbuffer[n] = '\0';
            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);
        }
    }
    ~UdpServer()
    {
        if(sockfd_>0) close(sockfd_);
    }
private:
    int sockfd_;//网络文件描述符
    string ip_;//0代表任意地址
    uint16_t port_;//服务进程的端口号
    bool isrunning_;
    unordered_map<string,struct sockaddr_in> online_user_;
};
cpp 复制代码
#include<iostream>
#include<cstdlib>
#include<unistd.h>
#include<strings.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<pthread.h>
#include"Terminal.hpp"

using namespace std;

void Usage(string proc)
{
    cout<<"\n\rUsage: "<<proc<<"serverip serverport\n"<<endl;
}
struct ThreadData
{
    struct sockaddr_in server;
    int sockfd;
    string serverip;
};

void* recv_message(void *args)
{
    OpenTerminal();
    ThreadData*td=static_cast<ThreadData*>(args);
    char buffer[1024];
    while(true)
    {
        //char buffer[1024];
        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)
        {
            //std::cout << "s: --" << s  << "--"<< endl; 
            buffer[s]=0;
            cerr<<buffer<<endl;//cerr的无缓冲特性,确保即时显示信息
        }
    }
}
void* send_message(void*args)
{
    ThreadData*td=static_cast<ThreadData*>(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@"  << std::endl;;
      
        getline(cin,message);//从标准输入读取用户输入的消息
        //将消息发送到服务器的地址(server结构体)
        sendto(td->sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&(td->server),len);
        message.clear();
    }
}

//使用多线程同时进行读写,实现功能解耦
// ./udpclient serverip serverport总共接受三个参数
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;
    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());//将点分十进制的 IP 字符串转换为网络字节序的整数
    socklen_t len=sizeof(td.server);
    td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(td.sockfd<0)
    {
        cout<<"socker error"<<endl;
        return 1;
    }

    td.serverip=serverip;

    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;
}
cpp 复制代码
#include"UdpServer.hpp"
#include<memory>
#include<cstdio>
#include<string>
#include<vector>

void Usage(string proc)
{
    cout<<"\n\rUsage: "<<proc<<" port[1024+]\n"<<endl;
}
//提供两个可选的客户端消息处理函数,供 Run 方法调用:
string Handler(const string&str)
{
    string res="server get a message: ";
    res+=str;
    cout<<res<<endl;
    return res;
}

bool SafeCheck(const string &cmd)//敏感关键词检测
{
    int safe=false;
    vector<string>key_word={
        "rm",
        "mv",
        "cp",
        "kill",
        "sudo",
        "unlink",
        "uninstall",
        "yum",
        "top",
        "while"
    };
    for(auto &word:key_word)
    {
        auto pos=cmd.find(word);//检查位置
        if(pos!=string::npos) return false;
    }
    return true;
}
string ExcuteCommand(const string &cmd)
{//执行系统命令(将客户端消息作为命令),通过 popen 创建管道读取命令输出,
//将输出结果返回给客户端(若命令不存在会返回错误信息)
    cout<<"get a request cmd: "<<cmd<<endl;
    if(!SafeCheck(cmd)) return "Bad man";
    
    FILE*fp=popen(cmd.c_str(),"r");//创建管道并以只读方式打开
    if(nullptr==fp)
    {
        perror("popen");
        return "error";
    }
    string result;
    char buffer[4096];
    while(true)
    {
        char*ok=fgets(buffer,sizeof(buffer),fp);//从管道读取数据到buffer
        if(ok==nullptr) break;
        result+=buffer;
    }
    pclose(fp);
    return result;
}
// ./udpserver port
int main(int argc,char*argv[])
{
    if(argc!=2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port=stoi(argv[1]);
    //使用智能指针 unique_ptr 管理 UdpServer 实例,
    //避免手动释放资源的风险:通过 new UdpServer(port) 传入端口号创建实例。
    unique_ptr<UdpServer> svr(new UdpServer(port));
    svr->Init();
    svr->Run();//启动服务器
    return 0;
}
  • Terminal.hpp
cpp 复制代码
#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<cstdio>
using namespace std;
string terminal="/dev/pts/2";
int OpenTerminal()
{
    int fd=open(terminal.c_str(),O_WRONLY);
    if(fd<0)
    {
        cerr<<"open terminal error"<<endl;
        return 1;
    }
    dup2(fd,2);//将标准输出重定向到伪终端当中
    close(fd);
    return 0;
}

其余部分与上文基础版相同,这里注意makefile中要添加-lpthread链接线程库信息,下图为不同终端实现的打印结果

  • 不同终端的重定向打印工作不仅可以用Terminal.hpp实现,还可以命令行实现
相关推荐
罗政3 小时前
【免费】轻量级服务器centos监控程序+内存+cpu+nginx+适合小型站长使用
服务器·nginx·centos
white-persist3 小时前
JWT 漏洞全解析:从原理到实战
前端·网络·python·安全·web安全·网络安全·系统安全
数据与人工智能律师4 小时前
解码Web3:DeFi、GameFi、SocialFi的法律风险警示与合规路径
大数据·网络·人工智能·云计算·区块链
CryptoRzz4 小时前
欧美(美股、加拿大股票、墨西哥股票)股票数据接口文档
java·服务器·开发语言·数据库·区块链
xingxing_F4 小时前
Network Radar for Mac 网络扫描管理软件
网络·macos
arvin_xiaoting4 小时前
#zsh# #Ubuntu# 一键安装zsh、oh-my-zsh、常用插件
linux·ubuntu·elasticsearch
wanhengidc4 小时前
巨椰云手机引领未来
运维·服务器·网络·游戏·智能手机
wanhengidc4 小时前
云手机的真实体验感怎么样
运维·服务器·安全·游戏·智能手机
脏脏a4 小时前
【Linux】Linux工具漫谈:yum 与 vim,高效操作的 “左膀右臂”
linux·运维·服务器