【Linux网络篇】:Socket网络套接字以及简单的UDP网络程序编写

发布于:2025-05-25 ⋅ 阅读:(21) ⋅ 点赞:(0)

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:Linux篇–CSDN博客

在这里插入图片描述

网络编程套接字

一.预备知识

1.理解源IP地址和目的IP地址

在IP协议层的数据报中,有两个IP地址,分别叫做源IP地址和目的IP地址。

源IP地址:发送数据包的设备的IP地址;告诉接收方数据包从哪里来。

目的IP地址:接收数据包的设备的IP地址;路由器根据目的IP地址决定数据包的转发路径。

2.认识端口号

先回答一个问题:在进行网络通信的时候,是不是我们的两台机器在进行通信呢?

首先网络协议栈中的下三层(传输层,网络层,数据链路层),主要解决的是数据安全可靠的送到远端机器;安全的发送不是目的,主要目的是收到数据后进行加工处理;而用户需要使用应用层软件,完成数据发送和接受,使用软件首先要启动软件,软件启动后,在系统层面就是一个进程

所以日常网络通信的本质就是进程间的通信;通过网络协议栈,借助网络这个共享资源,实现两台不同的主机上的两个不同的进程进行通信

而一台设备上可能同时运行多个网络应用程序(比如浏览器,邮件客户端,游戏服务器等),这时候传输层就需要明确知道当前发送的数据包具体要交给哪个程序处理,这里就需要借助端口号来实现。

  • 1.端口号是传输层协议的内容

    端口号是一个2字节16位的整数;可以唯一的标识当前设备上的一个网络应用程序

    而IP地址能够表示唯一的一台设备

    两者结合使用:共同确定数据包的最终目的地—哪台设备上的哪个进程应该接受或发送数据;这种技术就是套接字

    IP地址+端口号 = 套接字(Socket)

    套接字是网络通信中的端点,格式为:IP地址:端口号

  • 2.如何理解端口号和进程PID

    在系统中,PID表示唯一的一个进程,而此处端口号也是表示唯一的一个进程,那为什么网络通信时不直接用PID,而是要用端口号?

    最容易理解的一点就是:PID属于操作系统内部的进程管理,是系统模块的;而端口号则是用来网络通信中定位目标进程的,是网络模块的;两者不同的用途,实现系统和网络模块之间的解耦,满足模块之间低耦合的要求。

    此外,一个进程可以绑定多个端口号;但是一个端口号不能被多个进程绑定(因为不能满足唯一性)

  • 3.理解源端口号和目的端口号

    传输协议层(TCPUDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号,就是在描述**”数据是谁发的,要发给谁“**。

    源端口号

    • 标识发送数据包的进程
    • 通常是动态分配的临时端口,用于客户端发起请求;
    • 作用:确保服务器返回的相应能正确回到发起请求的进程

    目的端口号

    • 标识接收数据包的进程
    • 通常是固定端口
    • 作用:告诉目标主机将数据包交给哪个进程处理

3.初步认识TCP协议和UDP协议

此处先对TCP(传输控制协议)以及UDP(用户数据协议)有一个直观的认识;后面再详细讲解细节问题。

TCP协议

  • 传输层协议

  • 有连接

    简单理解就是打电话前需要先拨号,双方”接通“后才能对话;通信前需要建立一条”专属通道“,结束后要挂断。

  • 可靠传输

    传输过程中数据不会丢失,如果丢失,TCP协议可以重新发送;数据顺序不乱,TCP保证数据按序到达;确认机制,数据传输完后,需要等待对方确认收到数据才能结束,否则会一直重传。

  • 面向字节流

UDP协议

  • 传输层协议

  • 无连接

    简单理解就是直接发送短信或邮件,无需拨号或等待对方接听,不关心对方是否收到。

  • 不可靠传输

    数据传输过程中可能丢包,数据可能被丢弃,但发送的并不知道(没有重传机制);顺序混乱,数据可能乱序到达;无确认,发送完即结束,对方是否收到无法确认。

  • 面向数据报

4.网络字节序

1.什么是字节序?

当一个多字节的数据(比如一个16位的短整型short或一个32位的整形int)存储在内存中时,他的字节有两种排列方式:

  • 大端序:高位字节存储在内存的低地址,低位字节存储在内存的高地址;
  • 小端序:低位字节存储在内存的低地址,高位字节存储在内存的高地址;

例如,一个16位的整数0x1234(十进制的4660):

  • 高位字节是0x12
  • 低位字节是0x34

在内存中(假设起始地址是0x1000):

  • 大端序存储

    • 地址0x10000x12
    • 地址0x10010x34
  • 小端序存储

    • 地址0x10000x34
    • 地址0x10010x12

2.什么是网络字节序?

由于不同计算机的字节序可能不同,如果直接在网络上传输多字节数据,接收方可能会错误的解释这些数据。为了解决这个问题,TCP/IP协议栈规定了一个统一的网络字节序,这个标准就是大端序

所有在网络上传输的多字节数据(比如端口号,IP地址等)都必须转换为网络字节序(大端序)进行传输。接收方收到数据后,如果本机字节序与网络字节序不同,就要将其转换为本机字节序。

3.为什么需要转换函数?

  • 发送数据时:如果多字节数据,需要从主机字节序转换为网络字节序。
  • 接收数据时:如果是多字节数据,需要从网络字节序转换为主机字节序。

4.网络字节序转换函数

C语言提供了一组标准函数来进行主机字节序和网络字节序之间的转换。这些函数名中的h代表"host",n代表"network",s代表"short"(16位),l代表"long"(32位)

头文件

#include <arpa/inet.h>

函数列表

  • htons:从主机字节序到网络字节序(短整型16位)

    uint16_t htons(uint16_t hostshort);
    
  • htonl:从主机字节序到网络字节序(长整型32位)

    uint32_t htonl(uint32_t hostlong);
    
  • ntohs:从网络字节序到主机字节序(短整型16位)

    uint16_t ntohs(uint16_t netshort);
    
  • ntohl:从网络字节序到主机字节序(长整型32位)

    uint32_t ntohl(uint32_t netlong);
    

5.sockaddr结构

socket API是一层抽象的网络编程接口(具体的函数后面讲解UDP和TCP时分别讲解),适用于各种底层网络协议,比如IPv4,IPv6。然而,各种网络协议的地址格式并不相同

具体有以下三种:

1.struct sockaddr

作用

struct sockaddr通用的套接字地质结构体,用于在socker API中传递地址参数。它本身并不包含具体的地址信息,而是作为其他地址结构体(比如struct sockaddr_instruct sockaddr_un)的”父类“。

定义

struct sockaddr {
    sa_family_t sa_family;   // 地址族(如 AF_INET、AF_UNIX 等)
    char        sa_data[14]; // 地址数据(具体内容由子类决定)
};

说明

  • sa_family指明了地址类型(比如IPv4,UNIX域等)。
  • sa_data是一个通用的字节数组,具体内容由实际的地址类型决定。
  • 在实际使用时,通常将具体的地质结构体(比如sockaddr_in)强制类型转换为sockaddr*传递给socket API。

2.struct sockaddr_in

作用

struct sockaddr_in专门用于IPv4网络地址的结构体,包含了IP地址和端口号等信息。常用于基于IPv4的网络通信(比如UDP,TCP)。

定义

#include <netinet/in.h>
struct sockaddr_in {
    sa_family_t    sin_family; // 地址族,必须为 AF_INET
    in_port_t      sin_port;   // 端口号(网络字节序)
    struct in_addr sin_addr;   // IPv4 地址(网络字节序)
    unsigned char  sin_zero[8];// 填充字节,保证结构体大小与 sockaddr 一致
};
  • sin_family:地址族,必须设置为AF_INET
  • sin_port:端口号,需用htons()转换为网络字节序。
  • sin_addr:IPv4地址,需用inet_addr()inet_pton()转换为网络字节序。
  • sin_zero:填充字段,无实际意义,只是为了结构体对齐。

3.struct sockaddr_un

作用

struct sockaddr_un是用于本地(UNIX域)套接字通信的结构体,常用于同一台主机上的进程通信,而不经过网络协议栈。

定义

#include <sys/un.h>
struct sockaddr_un {
    sa_family_t sun_family;              // 地址族,必须为 AF_UNIX
    char        sun_path[108];           // 文件系统路径,表示本地套接字文件
};
  • sun_family:地址族,必须设置为AF_UNIX
  • sun_path:本地套接字文件路径,最大长度一般为108字节。

总结与区别

  • sockaddr是通用”父类“,实际用时需强转。
  • sockaddr_in用于IPv4网络通信。
  • sockaddr_un用于本地(UNIX域)套接字通信。

在实际编程中,API要求struct sockaddr*,传递struct sockaddr_in*struct sockaddr_un*时需要强制类型转换,这是网络编程的常见用法。

在这里插入图片描述

二.简单的UDP网络程序

相关接口

1.socket函数

int socket(int domain, int type, int protocol);
  • 功能:创建套接字
  • 参数
    • domain:协议族,比如AF_INET(IPv4);
    • type:套接字类型,SOCK_DGRAM(UDP),SOCK_STREAM(TCP);
    • protocol:协议,通常为0;
  • 返回值:成功返回套接字描述符sockfd(类似于文件描述符),后续所有的操作都依赖这个描述符;失败返回-1。

2.bind函数

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 功能:绑定地址和端口

  • 参数

    • sockfd:套接字描述符
    • addr:地址结构体指针
    • addrlen:地址结构体长度
  • 返回值:成功返回0,失败返回-1

  • 使用示例

    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(8080);  // 端口号
    local.sin_addr.s_addr = htonl(INADDR_ANY);  // 任意IP
    
    if (bind(sockfd, (struct sockaddr*)&local, sizeof(local)) < 0) {
        perror("bind error");
        return -1;
    }
    

3.sendto函数

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
  • 功能:发送数据

  • 参数

    • sockfd:套接字描述符
    • buf:要发送的数据
    • len:发送数据的长度
    • flags:发送标志,通常为0
    • dest_addr:目标地址结构体
    • addrlen:地址结构体长度
  • 返回值:成功返回发送的字节数,失败返回-1。

  • 注意事项

    • 如果是服务端使用该函数将数据发送给客户端,该函数参数中的地址结构体填充的就是客户端的相关信息;
    • 反之,客户端发送给服务端,填充的就是服务端的信息。
  • 使用示例

    struct sockaddr_in client;
    client.sin_family = AF_INET;
    client.sin_port = htons(8080);
    client.sin_addr.s_addr = inet_addr("127.0.0.1");
    
    char buffer[] = "Hello";
    sendto(sockfd, buffer, strlen(buffer), 0,
           (struct sockaddr*)&client, sizeof(client));
    

4.recvfrom函数

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, sockaddr *src_addr, socklen_t *addrlen);
  • 功能:接收数据

  • 参数

    • sockfd:套接字描述符
    • buf:用来接收数据的缓冲区
    • len:缓冲区的长度
    • flags:接收标志,通常为0
    • dest_addr:源地址结构体,输出型参数,用来获取发送数据一方的地址结构体信息
    • addrlen:地址结构体长度指针,也是输出型参数,用来获取发送数据一方的地址结构体长度
  • 返回值:成功返回接收的字节数,失败返回-1。

  • 注意事项

    • 如果是服务端调用该函数接收客户端发送的数据,地址结构体中就是客户端的信息,用来之后向客户端发送数据;
    • 如果是客户端调用该函数接收服务端发送的,地址结构体中就是服务端的信息,用来之后向服务端发送数据。
  • 使用示例

    char buffer[1024];
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    
    ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer), 0,
                         (struct sockaddr*)&client, &len);
    if (n > 0) {
        buffer[n] = 0;
        printf("收到数据:%s\n", buffer);
    }
    

5.close函数

int close(int sockfd);
  • 功能:关闭套接字
  • 参数
    • sockfd:要关闭的套接字描述符
  • 返回值:成功返回0;失败返回-1。

代码实现

基于上面的预备知识以及相关接口,实现一个自己的,可以相互发送接受数据的服务端与客户端。

服务端:udpserver.hpp

#pragma once

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

#define SIZE 1024

using func_t = std::function<std::string(const std::string &)>;

Log log;

enum{
    SOCKET_ERR=1,
    INADDR_ERR,
    BIND_ERR,
    PORT_ERR,
};

const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";

class UDPServer{
public:
    UDPServer(const uint16_t port=defaultport,const std::string ip=defaultip)
    :_sockfd(0),_port(port),_ip(ip),_isrunning(false)
    {
        // 检查端口号是否合法
        if(_port < 1024){
            log(Fatal, "Port number %d is too low, please use a port number > 1024", _port);
            exit(PORT_ERR);
        }
    } 

    void Init(){
        // 1.创建udp socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0){
            log(Fatal, "server socket create error, sockfd: %d", _sockfd);
            exit(SOCKET_ERR);
        }
        log(INFO, "server socket create success, sockfd: %d", _sockfd);

        // 2.连接udp socket
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);     // 端口号从主机字节序转换为网络字节序
        
        // 检查 IP 地址是否有效
        if (_ip == "0.0.0.0") {
            local.sin_addr.s_addr = htonl(INADDR_ANY);  // 监听所有网络接口
        } else {
            local.sin_addr.s_addr = inet_addr(_ip.c_str());
            if (local.sin_addr.s_addr == INADDR_NONE) {
                log(Fatal, "Invalid IP address: %s", _ip.c_str());
                exit(INADDR_ERR);
            }
        }

        // 将创建的socket与本地的IP地址和端口号绑定
        if(bind(_sockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){
            log(Fatal, "server bind error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        log(INFO, "server bind success, errno: %d, strerror: %s", errno, strerror(errno));
    }

    void Run1(func_t fun)
    {
        _isrunning = true;
        char buffer[SIZE];
        while(_isrunning){
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // recvform的后两个参数位输出型参数
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &len);
            if(n < 0){
                log(Warning, "server recvfrom error, errno: %d, strerror: %s", errno, strerror(errno));
                continue;
            }
            buffer[n] = 0;
            std::string info = buffer;

            // 模拟一次数据处理
            std::string echo_string = fun(info);
            std::cout << echo_string << std::endl;

            // 将处理后的数据发送到目标地址
            sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const struct sockaddr *)&client, len);
        }
    }

    ~UDPServer()
    {
        if(_sockfd > 0){
            close(_sockfd);
        }
    }

private:
    int _sockfd;     // 网络文件描述符
    uint16_t _port;  // 端口号
    std::string _ip; // ip地址
    bool _isrunning;
};

主程序:main.cc

#include "udpserver.hpp"
#include <iostream>
#include <memory>

void Usage(std::string proc){
    std::cout << "\n\rUsage: " << proc << " port[1024+]\n"
              << std::endl;
}

std::string Handler(const std::string &str){
    std::string ret = "Server get a message# ";
    ret += str;
    return ret;
}

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->Run1(Handler);

    return 0;
}

客户端:udpclient

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <string>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"

#define SIZE 1024

Log log;

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

int main(int argc, char *argv[]){
    // ./udpclient serverip serverport
    if (argc != 3){
        Usage(argv[0]);
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::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());
    socklen_t len = sizeof(server);

    // 创建client socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0){
        log(Fatal, "client socket create error, errno: %d, strerror: %s", errno, strerror(errno));
        exit(1);
    }
    log(INFO, "client socket create success, sockfd: %d", sockfd);

    // client bind由系统完成 在首次发送数据时bind

    std::string message;  
    char buffer[SIZE];
    while(true){
        std::cout << "Please Enter@ ";
        getline(std::cin, message);

        // 1.发送数据到server
        sendto(sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&server, len);
        //std::cout << " sendto aready " << std::endl;

        // 2.从server接收数据
        struct sockaddr_in temp;
        socklen_t len_temp = sizeof(temp);
        ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len_temp);
        if(n < 0){
            log(Warning, "client recvfrom error, errno: %d, strerror: %s", errno, strerror(errno));
            continue;
        }
        buffer[n] = 0;
        std::cout << buffer << std::endl;
    }

    close(sockfd);

    return 0;
}

注意事项

1.IP地址

在服务端实现中,对IP地址初始化时,一般建议设置成0.0.0.0INADDR_ANY)。

原因是:可以监听所有网络接口,可以接受来自任何网络接口的数据,包括本地回环接口(127.0.0.1)。

除此之外还可以变得更加灵活,服务器不需要知道具体的IP地址,可以适应多网卡环境。

因为这里使用的是云服务器,如果在初始化时,设置成指定的IP地址,会出现以下错误:

在这里插入图片描述

常见原因:

  • 指定的IP地址不存在
  • 指定的IP地址不是本机的IP
  • 网络接口未启用
  • 指定的IP地址格式错误

所以在平常的使用或开发时一般建议使用INADDR_ANY

2.端口号

在服务端实现时,对端口号的初始化值一般建议大于1024,因为使用小于1024的端口号需要root权限;

除此之外,如果使用的是云服务器,还需要在控制台的安全组中开放对应的端口。

如果出现以下错误:

在这里插入图片描述

常见原因:

  • 使用特权端口(<1024)没有root权限
  • 文件权限不足
  • 目录权限不足
  • 系统安全策略限制

上面就是关于IP地址和端口号的注意事项,实际使用时,一定要注意这几点。

效果演示
在这里插入图片描述

应用场景

1.执行客户端发送的指令

主程序

修改服务器处理信息时的回调函数为执行指令:

#include "udpserver.hpp"
#include <iostream>
#include <memory>
#include <vector>
#include <string>

void Usage(std::string proc){
    std::cout << "\n\rUsage: " << proc << " port[1024+]\n"
              << std::endl;
}


// 简单的发送信息
std::string Handler(const std::string &str){
    std::string ret = "Server get a message# ";
    ret += str;
    std::cout << ret << std::endl;
    return ret;
}


// 应用场景:执行客户端发送的指令
bool SafeCheck(const std::string &cmd){
    std::vector<std::string> key_word = {
        "rm",
        "mv",
        "cp",
        "kill",
        "sudo",
        "unlink",
        "uninstall",
        "yum",
        "top",
        "while"};
    for(auto word : key_word){
        auto it = cmd.find(word);
        if(it!=std::string::npos){
            return false;
        }
    }

    return true;
}
std::string ExcuteCommand(const std::string &cmd){
    std::cout << "server get a command: " << cmd << std::endl;

    // 判断输入的指令的是否危险
    if (!SafeCheck(cmd)){
        return "Bad Command";
    }

    // 创建一个管道并执行输入的指令
    FILE *fp = popen(cmd.c_str(), "r");
    if (fp == nullptr){
        return "Command execute failed!";
    }

    // 从管道中读取内容,直到读取到空
    std::string ret;
    char buffer[4096];
    while(true){
        char *ok = fgets(buffer, sizeof(buffer), fp);
        if (ok == nullptr){
            break;
        }
        ret += buffer;
    }

    pclose(fp);
    return ret;
}



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->Run1(Handler);
    svr->Run1(ExcuteCommand);

    return 0;
}

效果演示

在这里插入图片描述

2.Windows与Linux不同系统间的网络传输

在vs2022启动一个Windows的客户端:

#include <iostream>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>

#pragma warning(disable:4996)

#pragma comment(lib, "ws2_32.lib")

uint16_t serverport = 18080;
std::string serverip = "1.117.74.41";

int main() {
	WSADATA wsd;
	WSAStartup(MAKEWORD(2, 2), &wsd);

    // 填充服务器的网络地址结构体
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    
    SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == SOCKET_ERROR) {
        std::cout << "socker error" << std::endl;
        exit(1);
    }

    std::string message;
    char buffer[1024];
    while (true) {
        std::cout << "Please Enter@ ";
        getline(std::cin, message);

        // 1.发送数据到server
        sendto(sockfd, message.c_str(), message.size(), 0, (const struct sockaddr*)&server, sizeof(server));
        //std::cout << " sendto aready " << std::endl;

        // 2.从server接收数据
        struct sockaddr_in temp;
        int len = sizeof(temp);
        int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);
        if (n < 0) {
            std::cout << "revform error" << std::endl;
            exit(2);
        }
        buffer[n] = 0;
        std::cout << buffer << std::endl;
    }

    closesocket(sockfd);
    WSACleanup();
	return 0;
}

左边是Windows客户端,右边是Linux服务端

在这里插入图片描述

3.多人聊天

服务端代码修改

修改内容:增加一个哈希表用来存储已经发送过信息的用户,根据用户的IP地址来判断是否是新用户,如果不存在哈希表中就是新用户,添加到哈希表中;服务器处理完某个用户发送的信息后,将该信息发送给哈希表中的所有用户。

#pragma once

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

#define SIZE 1024

using func_t = std::function<std::string(const std::string &)>;

Log log;

enum{
    SOCKET_ERR=1,
    INADDR_ERR,
    BIND_ERR,
    PORT_ERR,
};

const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";

class UDPServer{
private:
    void CheckUser(struct sockaddr_in &client, const uint16_t clientport, const std::string &clientip){
        auto it = online_user.find(clientip);
        if(it == 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 uint16_t clientport, const std::string &clientip){
        // 信息处理
        std::string message = "[";
        message += clientip;
        message += ":";
        message += std::to_string(clientport);
        message += "]# ";
        message += info;

        std::cout << "server get a message: " << message << std::endl;

        // 依次编译哈希表 将信息发送给每一个用户
        for(const auto &user : online_user){
            socklen_t len = sizeof(user.second);
            sendto(_sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)(&user.second), len);
        }
    }

public:
    UDPServer(const uint16_t port=defaultport,const std::string ip=defaultip)
    :_sockfd(0),_port(port),_ip(ip),_isrunning(false)
    {
        // 检查端口号是否合法
        if(_port < 1024){
            log(Fatal, "Port number %d is too low, please use a port number > 1024", _port);
            exit(PORT_ERR);
        }
    } 

    void Init(){
        // 1.创建udp socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0){
            log(Fatal, "server socket create error, sockfd: %d", _sockfd);
            exit(SOCKET_ERR);
        }
        log(INFO, "server socket create success, sockfd: %d", _sockfd);

        // 2.连接udp socket
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);     // 端口号从主机字节序转换为网络字节序
        
        // 检查 IP 地址是否有效
        if (_ip == "0.0.0.0") {
            local.sin_addr.s_addr = htonl(INADDR_ANY);  // 监听所有网络接口
        } else {
            local.sin_addr.s_addr = inet_addr(_ip.c_str());
            if (local.sin_addr.s_addr == INADDR_NONE) {
                log(Fatal, "Invalid IP address: %s", _ip.c_str());
                exit(INADDR_ERR);
            }
        }

        // 将创建的socket与本地的IP地址和端口号绑定
        if(bind(_sockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){
            log(Fatal, "server bind error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        log(INFO, "server bind success, errno: %d, strerror: %s", errno, strerror(errno));
    }

    void Run1(func_t fun)
    {
        _isrunning = true;
        char buffer[SIZE];
        while(_isrunning){
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // recvform的后两个参数位输出型参数
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &len);
            if(n < 0){
                log(Warning, "server recvfrom error, errno: %d, strerror: %s", errno, strerror(errno));
                continue;
            }
            buffer[n] = 0;
            std::string info = buffer;

            // 模拟一次数据处理
            std::string echo_string = fun(info);

            // 将处理后的数据发送到目标地址
            sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const struct sockaddr *)&client, len);
        }
    }

    // 多用户聊天测试
    void Run2(){
        _isrunning = true;
        char buffer[SIZE];
        while(_isrunning){
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // recvform的后两个参数位输出型参数
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &len);
            if(n < 0){
                log(Warning, "server recvfrom error, errno: %d, strerror: %s", errno, strerror(errno));
                continue;
            }
            buffer[n] = 0;

            std::string info = buffer;
            uint16_t clientport = ntohs(client.sin_port);
            std::string clientip = inet_ntoa(client.sin_addr);

            // 检查当前用户是否已经在哈希表中
            CheckUser(client, clientport, clientip);

            // 将当前信息发送给所有用户
            BroadCast(info, clientport, clientip);
        }
    }

    ~UDPServer()
    {
        if(_sockfd > 0){
            close(_sockfd);
        }
    }

private:
    int _sockfd;     // 网络文件描述符
    uint16_t _port;  // 端口号
    std::string _ip; // ip地址
    bool _isrunning;
    std::unordered_map<std::string, struct sockaddr_in> online_user;
};

客户端代码修改

修改内容:平常使用微信,QQ等群聊时,即使我们不在群里发送消息我们也会收到其他用户发送的消息;所以用户在客户端的发送消息和接收消息一定是分开的,所以需要将上面的单进程客户端修改为多线程,分别处理消息的发送和接收。

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <string>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
//#include "log.hpp"

#define SIZE 1024

//Log log;

struct ThreadData{
    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){
    ThreadData *td = static_cast<ThreadData *>(args);
    char buffer[SIZE];
    while(true){
        // 2.从server接收数据
        memset(buffer, 0, sizeof(buffer));
        struct sockaddr_in temp;
        socklen_t len_temp = sizeof(temp);

        ssize_t n = recvfrom(td->sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len_temp);
        if(n < 0){
            //log(Warning, "client recvfrom error, errno: %d, strerror: %s", errno, strerror(errno));
            continue;
        }
        buffer[n] = 0;
        // 将收到的信息打印到标准错误流2中 然后再重定向到终端设备上 模拟同一界面的发消息和收消息
        std::cerr << buffer << std::endl;
    }
}

void *send_message(void *args){
    ThreadData *td = static_cast<ThreadData *>(args);
    std::string message;
    socklen_t len = sizeof(td->server);

    std::string welcome = td->serverip;
    welcome += " coming ...";
    sendto(td->sockfd, welcome.c_str(), welcome.size(), 0, (const struct sockaddr *)&(td->server), len);

    while(true){
        std::cout << "Please Enter@ ";
        getline(std::cin, message);

        // 1.发送数据到server
        sendto(td->sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&(td->server), len);
    }
}

int main(int argc, char *argv[]){
    // ./udpclient serverip serverport
    if (argc != 3){
        Usage(argv[0]);
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 填充服务器的网络地址结构体
    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());
    td.serverip = serverip;

    // 创建client socket
    td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(td.sockfd < 0){
        //log(Fatal, "client socket create error, errno: %d, strerror: %s", errno, strerror(errno));
        exit(1);
    }
    //log(INFO, "client socket create success, sockfd: %d", td.sockfd);

    // client bind由系统完成 在首次发送数据时bind

    // 多线程执行数据的发送和接收
    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;
}

主程序修改

服务器启动后调用另一个运行函数。

在这里插入图片描述

效果演示

用户一:

用户一的IP地址是127.0.0.1,本地用户进行测试;

左边上侧用一个终端表示聊天框,下侧用另一个终端表示输入框;右边则是正在运行的服务器。

在这里插入图片描述

用户二:

用户二的IP地址是1.117.74.41,另一个主机用户进行测试;

上是聊天框,下是输入框。

在这里插入图片描述

以上就是关于Socket网络套接字以及简单UDP网络程序编写的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!


网站公告

今日签到

点亮在社区的每一天
去签到