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

news2025/5/25 19:00:10

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

在这里插入图片描述

文章目录

  • 网络编程套接字
  • 一.预备知识
    • 1.理解源IP地址和目的IP地址
    • 2.认识端口号
    • 3.初步认识TCP协议和UDP协议
    • 4.网络字节序
    • 5.sockaddr结构
  • 二.简单的UDP网络程序
    • 相关接口
    • 代码实现
    • 应用场景

网络编程套接字

一.预备知识

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网络程序编写的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2385542.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学习路之uniapp--unipush2.0推送功能--给自己发通知

学习路之uniapp--unipush2.0推送功能--给自己发通知 一、绑定云空间及创建云函数二、编写发送界面三、效果后期展望&#xff1a; 一、绑定云空间及创建云函数 package.json {"name": "server-push","dependencies": {},"main": "…

leetcode hot100刷题日记——12.反转链表

解答&#xff1a; /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr) {}* ListNode(int x, ListNode *next) : val(x), next(n…

《Python语言程序设计》第4章第8题3个个位数之间比大小。‘a小于b而b大于c’这是最有漏洞的一个对比,请问我如何判断a和c

升序来做这个题 比如123就变成321 需要比对3个数 这不是比对2个数。a和b比对 我们可以直接写 if a>b: print(ab) else print(ba) 但是现在是3个数abc 如果进行if比对呢 if a > b >c: print(a,b,c) elif a < b >c: print(bca) … 简洁的代码变成了复杂的代码段。…

Selenium 测试框架 - Python

🚀Selenium Python 实战指南:从入门到进阶 Selenium 是 Web 自动化测试中最受欢迎的工具之一,支持多种浏览器和语言。本文将从环境搭建到多浏览器兼容、测试框架集成、元素定位方式、常用操作、浏览器配置等多个方面进行详细讲解,并分享常见的最佳实践建议。 📦一、环境…

RNN GRU LSTM 模型理解

一、RNN 1. 在RNN中&#xff0c; 二、GRU 1. GRU是为了解决RNN 梯度消失引入的改良模型&#xff0c; 2. GRU 通过门控 Gamma_r Gamma_u 两个变量&#xff0c;实现了对于过往记忆的筛选&#xff1a;这种机制使得GRU能够灵活地决定何时“忘记”过去的信息以及何时“记住”新的…

【MC】红石比较器

在《我的世界》&#xff08;Minecraft&#xff09;中&#xff0c;红石比较器&#xff08;Redstone Comparator&#xff09; 是一种高级红石元件&#xff0c;主要用于 检测、比较或处理信号强度&#xff0c;同时还能与容器、特定方块互动。 红石比较器有两种模式&#xff1a; 比…

红黑树简单模拟实现

定义成员变量旋转insert以234树的角度来待插入操作具体代码 完整代码 我们前面实现了 二叉搜索树和 AVL树。 其中AVL树是二叉搜索树的改进&#xff0c;但是有些人觉得二叉树搜索的插入调整太频繁了&#xff0c;或者说平衡条件过于苛刻。 于是人们放松了左右子树高度差的限制&…

豪越科技:消防应急装备智能仓储管理新变革

在消防救援工作中&#xff0c;消防装备无疑是消防员们与火灾等灾害顽强对抗的关键“武器”。然而&#xff0c;传统的消防装备管理模式长期以来饱受诸多痛点的困扰&#xff0c;严重影响着消防工作的高效开展和救援效果。 在过去&#xff0c;装备丢失的情况时有发生。由于缺乏有效…

如何设计Agent的记忆系统

最近看了一张画Agent记忆分类的图 我觉得分类分的还可以&#xff0c;但是太浅了&#xff0c;于是就着它的逻辑&#xff0c;仔细得写了一下在不同的记忆层&#xff0c;该如何设计和选型 先从流程&#xff0c;作用&#xff0c;实力和持续时间的这4个维度来解释一下这几种记忆&am…

毕业论文格式(Word)

目录 Word目录怎么自动生成&#xff1f;快速生成试试这3个方法&#xff01; - 知乎https://zhuanlan.zhihu.com/p/692056836目录生成需要先设置标题样式&#xff0c;这个不仅是目录生成需要&#xff0c;和后续的图表也有关系。 最好不要自己创建新的样式&#xff0c;而是在现有…

学习STC51单片机14(芯片为STC89C52RC)

接下来我们进入学会了HC—SR04 还有舵机那么现在我们将他们融合在一起&#xff0c;用超声波来引导舵机的转动 我们这个最后的成果是做一个智能垃圾桶 成品是这样的&#xff0c;是不是可有意思了 成品视频 现在我们将舵机的代码和超声波测距模块的代码整合到一起&#xff0c;实…

基于CodeBuddy实现本地网速的实时浏览小工具

本文所使用的 CodeBuddy 免费下载链接&#xff1a;腾讯云代码助手 CodeBuddy - AI 时代的智能编程伙伴 前言 在数字化浪潮席卷全球的今天&#xff0c;网络已成为人们生活和工作中不可或缺的基础设施。无论是在线办公、学习、娱乐&#xff0c;还是进行大数据传输和云计算&…

stable diffusion论文解读

High-Resolution Image Synthesis with Latent Diffusion Models 论文背景 LDM是Stable Diffusion模型的奠基性论文 于2022年6月在CVPR上发表 传统生成模型具有局限性&#xff1a; 扩散模型&#xff08;DM&#xff09;通过逐步去噪生成图像&#xff0c;质量优于GAN&#x…

计算机网络(3)——传输层

1.概述 1.1 传输层的服务和协议 (1)传输层为允许在不同主机(Host)上的进程提供了一种逻辑通信机制 (2)端系统(如手机、电脑)运行传输层协议 发送方&#xff1a;将来自应用层的消息进行封装并向下提交给 网络层接收方&#xff1a;将接收到的Segment进行组装并向上提交给应用层 …

LangChain构建RAG的对话应用

目录 Langchain是什么&#xff1f; LangSmith是什么&#xff1f; ​编辑 使用Python构建并使用AI大模型 数据解析器 提示模版 部署 记忆功能 Chat History -- 记忆 代码执行流程&#xff1a; 流式输出 构建向量数据库和检索器 检索器 代码执行流程 LLM使用检索器…

目标检测DN-DETR(2022)详细解读

文章目录 gt labels 和gt boxes加噪query的构造attention maskIS&#xff08;InStability&#xff09;指标 在DAB-Detr的基础上&#xff0c;进一步分析了Detr收敛速度慢的原因&#xff1a;二分图匹配的不稳定性&#xff08;也就是说它的目标在频繁地切换&#xff0c;特别是在训…

嵌入式培训之系统编程(四)进程

一、进程的基本概念 &#xff08;一&#xff09;定义 进程是一个程序执行的过程&#xff08;也可以说是正在运行的程序&#xff09;&#xff0c;会去分配内存资 源&#xff0c;cpu的调度&#xff0c;它是并发的 &#xff08;二&#xff09;PCB块 1、PCB是一个结构体&#x…

天文数据处理:基于CUDA的射电望远镜图像实时去噪算法(开源FAST望远镜数据处理代码解析)

一、射电天文数据处理的挑战与CUDA加速的必要性 作为全球最大的单口径射电望远镜&#xff0c;中国天眼&#xff08;FAST&#xff09;每秒产生38GB原始观测数据&#xff0c;经预处理后生成数千万张图像。这些数据中蕴含的脉冲星、中性氢等天体信号常被高斯白噪声、射频干扰&…

VS编码访问Mysql数据库

安装 MySQL Connector/C 的开发包 libmysqlcppconn-dev是 MySQL Connector/C 的开发包&#xff0c;它的主要用途是让 C 开发者能够方便地在应用程序中与 MySQL 数据库进行交互。它提供了以下功能&#xff1a; 数据库连接&#xff1a;通过标准的 C 接口连接到 MySQL 数据库。S…

一周学会Pandas2 Python数据处理与分析-Pandas2数据合并与对比-pd.merge():数据库风格合并

锋哥原创的Pandas2 Python数据处理与分析 视频教程&#xff1a; 2025版 Pandas2 Python数据处理与分析 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili pd.merge()&#xff1a;数据库风格合并 **核心功能**&#xff1a;基于列值&#xff08;类似 SQL JOIN&#xff09;合…