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

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉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_INETin_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_UNIXchar        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);  // 任意IPif (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 1024using 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 socketstruct 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 1024Log log;void Usage(std::string proc){std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}int main(int argc, char *argv[]){// ./udpclient serverip serverportif (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 socketint 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由系统完成 在首次发送数据时bindstd::string message;  char buffer[SIZE];while(true){std::cout << "Please Enter@ ";getline(std::cin, message);// 1.发送数据到serversendto(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.发送数据到serversendto(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 1024using 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 socketstruct 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.发送数据到serversendto(td->sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&(td->server), len);}
}int main(int argc, char *argv[]){// ./udpclient serverip serverportif (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 sockettd.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.pswp.cn/diannao/84456.shtml

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

相关文章

Python爬虫实战:研究Newspaper框架相关技术

1. 引言 1.1 研究背景与意义 互联网的快速发展使得新闻信息呈现爆炸式增长&#xff0c;如何高效地获取和分析这些新闻数据成为研究热点。新闻爬虫作为一种自动获取网页内容的技术工具&#xff0c;能够帮助用户从海量的互联网信息中提取有价值的新闻内容。本文基于 Python 的 …

【node.js】实战项目

个人主页&#xff1a;Guiat 归属专栏&#xff1a;node.js 文章目录 1. 项目概览与架构设计1.1 实战项目&#xff1a;企业级电商管理系统1.2 技术栈选择 2. 项目初始化与基础架构2.1 项目结构设计2.2 基础配置管理 3. 用户服务实现3.1 用户服务架构3.2 用户模型设计3.3 用户服务…

Mybatis框架的构建(IDEA)

选择maven项目 修改设置 在设置中添加自定义代码模板 开始写代码 动态SQL语句的示例&#xff1a; pom文件&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"…

经济法-6-公司法律制度知识点

一、出资期限 1.有限责任公司&#xff1a;全体股东需在公司成立之日起5年内缴足认缴的注册资本 2.股份有限公司&#xff1a;以发起方式设立的&#xff0c;发起人需在公司登记前实缴全部股款 3.认缴期加速到期 公司不能清偿到期债务的&#xff0c;公司或者已到期债权的债权人…

jquery.table2excel方法导出

jquery提供了一个table2excel方法可以用来导出页面到xls等 $("#grid_595607").table2excel({exclude: ".noExport", // 排除类名为 noExport 的元素filename: "导出数据.xls",exclude_img: true, // 不导出图片exclude_links: true, // 不导…

echarts设置标线和最大值最小值

echarts设置标线和最大值最小值 基本ECharts图表初始化配置 设置动态的y轴范围&#xff08;min/max值&#xff09; 通过markPoint标记最大值和最小值点 使用markLine添加水平参考线 配置双y轴图表 自定义标记点和线的样式&#xff08;颜色、符号等&#xff09; 响应式调整图表大…

Java文件操作:从“Hello World”到“Hello File”

&#x1f50d; 开发者资源导航 &#x1f50d;&#x1f3f7;️ 博客主页&#xff1a; 个人主页&#x1f4da; 专栏订阅&#xff1a; JavaEE全栈专栏 文件 什么是文件&#xff1f; 广义&#xff1a;操作系统进行资源管理的一种机制&#xff0c;很多的软件/硬件资源&#xff0c;…

2025第三届黄河流域网络安全技能挑战赛--Crypto--WriteUp

2025第三届黄河流域网络安全技能挑战赛–Crypto–WriteUp Crypto sandwitch task from Crypto.Util.number import * import gmpy2 flag bflag{fake_flag} assert len(flag) 39 p getPrime(512) q getPrime(512) n p * q e 0x3 pad1 beasy_problem pad2 bHow_to_so…

三重天理论

第一重天&#xff1a;公理层&#xff08;形而上地基&#xff09; 这里构建的是人类理性的"操作系统"&#xff0c;公理作为不证自明的逻辑起点&#xff08;如矛盾律/同一律&#xff09;&#xff0c;恰似海德格尔所说的"存在之镜"。黑格尔辩证法在此显现为动…

2025年第八届广西大学生程序设计大赛(正式赛)题解(更新中)

知乎评价&#xff1a;如何评价2025年第八届GXCPC广西大学生程序设计大赛暨中国-东盟国际大学生程序设计大赛&#xff1f; 榜单&#xff1a;牛客比赛排名 题目链接&#xff1a;第八届广西大学生程序设计大赛暨2025邀请赛 TIP&#xff1a;提交处可查看别人过题代码 难度签到题普通…

WHAT - 兆比特每秒 vs 兆字节每秒

文章目录 Mbps 解释Mbps 和 MB/s&#xff08;兆字节每秒&#xff09;换算总结网络场景1. 在路由器设置中的 Mbps2. 在游戏下载时的 Mbps / MB/s总结 Mbps 解释 首先&#xff0c;Mbps 是一个常见的网络带宽单位&#xff0c;意思是&#xff1a; Megabits per second&#xff08;…

[C语言实战]C语言内存管理实战:实现自定义malloc与free(四)

[C语言实战]C语言内存管理实战&#xff1a;实现自定义malloc与free&#xff08;四&#xff09; 摘要&#xff1a;通过实现简化版的内存管理器&#xff0c;深入理解动态内存分配的核心原理。本文包含内存块设计、分配算法、空闲合并策略的完整实现&#xff0c;并附可运行的代码…

YOLOv8源码修改(5)- YOLO知识蒸馏(下)设置蒸馏超参数:以yolov8-pose为例

目录 前言 1. 不同蒸馏算法资源占用 2. 不动态调整蒸馏损失 2.1 训练定量化结果 2.1 训练结果可视化结果 3. 动态调整蒸馏损失权重及实验分析 3.1 余弦衰减和指数衰减 3.2 CWD蒸馏损失 3.3 MGD蒸馏损失 3.4 AT蒸馏损失 3.5 SKD和PKD蒸馏损失 4. 调权重心得总结 5…

历年华东师范大学保研上机真题

2025华东师范大学保研上机真题 2024华东师范大学保研上机真题 2023华东师范大学保研上机真题 在线测评链接&#xff1a;https://pgcode.cn/school?classification1 简单一位数代数式计算 题目描述 给一个小学生都会算的1位数与1位数运算的代数式&#xff0c;请你求出这个表…

Oracle 中 SHRINK 与 MOVE 操作的比较

Oracle 中 SHRINK 与 MOVE 操作的比较 在 Oracle 数据库中&#xff0c;SHRINK 和 MOVE 都是用于重组表和索引以减少空间碎片的重要操作&#xff0c;但它们在实现方式和适用场景上有显著区别。 SHRINK 操作 基本语法 ALTER TABLE table_name SHRINK SPACE [COMPACT] [CASCAD…

展锐 Android 15 锁定某个App版本的实现

Android 15 系统锁定Antutu版本的实现方法 在Android系统开发中,有时需要锁定特定应用的版本以确保系统稳定性或测试一致性。本文将介绍如何通过修改Android源码来锁定Antutu跑分软件的版本。 修改概述 这次修改主要涉及以下几个方面: 禁用产品复制文件的检查添加指定版本…

视频剪辑SDK定制开发技术方案与报价书优雅草卓伊凡

视频剪辑SDK定制开发技术方案与报价书-优雅草卓伊凡 一、项目概述 客户需求&#xff1a;开发一套跨平台&#xff08;Android/iOS/Uni-App&#xff09;视频剪辑SDK&#xff0c;包含AI字幕提取、转场特效、文字叠加、背景音乐、滤镜、背景替换、动态贴纸等功能。 报价范围&#…

BGP为什么要配置对等IP?

本文由deepseek生成&#xff0c;特此声明 一、为什么要配置对等体IP&#xff1f; 1. 明确标识邻居身份 路由协议需求&#xff1a;动态路由协议&#xff08;如BGP、OSPF、RIP&#xff09;需要路由器之间建立邻居关系以交换路由信息。配置对等体IP是为了唯一标识邻居路由器&…

Qt中配置文件读写

1. 保存分组数据到配置文件 #include <QSettings>void saveNetworkConfig() {QSettings settings("network.ini", QSettings::IniFormat);// 网络配置分组settings.beginGroup("Network");// 源地址配置settings.beginGroup("Source");se…

Linux 的编辑器--vim

1.Linux编辑器-vim使⽤ vi/vim的区别简单点来说&#xff0c;它们都是多模式编辑器&#xff0c;不同的是vim是vi的升级版本&#xff0c;它不仅兼容vi的所有指令&#xff0c;⽽且还有⼀些新的特性在⾥⾯。例如语法加亮&#xff0c;可视化操作不仅可以在终端运⾏&#xff0c;也可以…