【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.理解源端口号和目的端口号
传输协议层(
TCP
和UDP
)的数据段中有两个端口号,分别叫做源端口号和目的端口号,就是在描述**”数据是谁发的,要发给谁“**。源端口号:
- 标识发送数据包的进程;
- 通常是动态分配的临时端口,用于客户端发起请求;
- 作用:确保服务器返回的相应能正确回到发起请求的进程
目的端口号:
- 标识接收数据包的进程;
- 通常是固定端口;
- 作用:告诉目标主机将数据包交给哪个进程处理
3.初步认识TCP协议和UDP协议
此处先对TCP
(传输控制协议)以及UDP
(用户数据协议)有一个直观的认识;后面再详细讲解细节问题。
TCP协议:
传输层协议
有连接
简单理解就是打电话前需要先拨号,双方”接通“后才能对话;通信前需要建立一条”专属通道“,结束后要挂断。
可靠传输
传输过程中数据不会丢失,如果丢失,TCP协议可以重新发送;数据顺序不乱,TCP保证数据按序到达;确认机制,数据传输完后,需要等待对方确认收到数据才能结束,否则会一直重传。
面向字节流
UDP协议:
传输层协议
无连接
简单理解就是直接发送短信或邮件,无需拨号或等待对方接听,不关心对方是否收到。
不可靠传输
数据传输过程中可能丢包,数据可能被丢弃,但发送的并不知道(没有重传机制);顺序混乱,数据可能乱序到达;无确认,发送完即结束,对方是否收到无法确认。
面向数据报
4.网络字节序
1.什么是字节序?
当一个多字节的数据(比如一个16位的短整型short
或一个32位的整形int
)存储在内存中时,他的字节有两种排列方式:
- 大端序:高位字节存储在内存的低地址,低位字节存储在内存的高地址;
- 小端序:低位字节存储在内存的低地址,高位字节存储在内存的高地址;
例如,一个16位的整数0x1234
(十进制的4660):
- 高位字节是
0x12
; - 低位字节是
0x34
;
在内存中(假设起始地址是0x1000
):
-
大端序存储:
- 地址
0x1000
:0x12
; - 地址
0x1001
:0x34
;
- 地址
-
小端序存储:
- 地址
0x1000
:0x34
; - 地址
0x1001
:0x12
;
- 地址
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_in
,struct 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
:发送标志,通常为0dest_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
:接收标志,通常为0dest_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.0
(INADDR_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网络程序编写的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!
相关文章:

【Linux网络篇】:Socket网络套接字以及简单的UDP网络程序编写
✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨ ✨ 个人主页:余辉zmh–CSDN博客 ✨ 文章所属专栏:Linux篇–CSDN博客 文章目录 网络编程套接字一.预备知识1.理解源IP地址和目的IP地址2.认识端…...

学习路之uniapp--unipush2.0推送功能--给自己发通知
学习路之uniapp--unipush2.0推送功能--给自己发通知 一、绑定云空间及创建云函数二、编写发送界面三、效果后期展望: 一、绑定云空间及创建云函数 package.json {"name": "server-push","dependencies": {},"main": "…...
Java面向对象 一
系列文章目录 Java面向对象 二-CSDN博客 目录 系列文章目录 前言 一、初步认识面向对象 1.类和对象的简单理解 2.类的构成 二、类的实例化 1.对象的创建 2.对象的初始化 三、this引用的作用 四、构造方法 1.构造方法的提供 2.对象的构造 3.构造方法的重载 4.th…...
怎么开发一个网络协议模块(C语言框架)之(二) 数据结构设计
一、数据结构设计模板分析 (gdb) p gVrrpInstance $3 = { INT4 socketV4 = 107, .... vrrpStatisticsEntry_t SvrrpStatistics = {delIp4Count = 0, delIp6Count = 0, delIp4Error = 0, delIp6Error = 0, addIp4Count = 0, addIp6Count = 3, addIp4Error = 0, addIp6Error …...
30天自制操作系统day5(vram和显存)(GDT和IDT)(c语言结构体)(汇编-c)(ai辅助整理)
day5 harib02d c语言结构体的一些解释 struct BOOTINFO { char cyls, leds, vmode, reserve; short scrnx, scrny; char *vram; }; //最开始的struct命令只是把一串变量声明集中起来,统一叫做“struct BOOTINFO”。 //最初是1字节的变量cyls,接着是1字…...
【音频】drc 限幅器、多带限幅器、压缩器、多带压缩器
以下是关于 DRC 限幅器、多带限幅器、压缩器、多带压缩器的详细解释,它们均为音频处理领域的动态范围控制设备,主要用于调整音频信号的动态范围(即最大音量与最小音量的差值),以优化音质或满足特定播放需求: 一、DRC 限幅器(Dynamic Range Compression Limiter) 核心功…...

leetcode hot100刷题日记——12.反转链表
解答: /*** 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…...
osgEarth中视角由跟随模式切换到漫游模式后没有鼠标拖拽功能问题分析及解决方法
遇到了一个棘手的问题,就是在由跟随模式切换到漫游模式的时候,鼠标无法实现拖拽功能。后来发现是前面给自己挖的坑。 因为要实现鼠标点选某个模型后,模型需要变红色显示,所以添加了一个事件处理程序。 // 创建 场景中模型的点选功能 事件处理程序 ModelSelectionHandler* …...
STM32中断优先级分组有哪几种?
STM32中断优先级分组主要有以下5种: 分组0:所有16位用于子优先级,没有抢占优先级。此时可配置的子优先级为0~15,共16级,适用于系统中对中断实时性要求不高,且中断源较多,需要更多子优先级来区分不同中断的情况。分组1:最高1位用于抢占优先级,最低3位用于子优先级。可配…...

《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中, 二、GRU 1. GRU是为了解决RNN 梯度消失引入的改良模型, 2. GRU 通过门控 Gamma_r Gamma_u 两个变量,实现了对于过往记忆的筛选:这种机制使得GRU能够灵活地决定何时“忘记”过去的信息以及何时“记住”新的…...
AutoCompose - 携程自动编排原理 -【编排关系DAG的构建】
AutoCompose - 携程自动编排原理 -【编排关系DAG的构建】 前言一. Spring / SpringBoot 的兼容✅ spring.factories 文件🧩 特点📄 示例 ✅ META-INF/spring/ 目录下的文件(Spring Boot 2.4 新特性)🧩 特点Ὄ…...

【MC】红石比较器
在《我的世界》(Minecraft)中,红石比较器(Redstone Comparator) 是一种高级红石元件,主要用于 检测、比较或处理信号强度,同时还能与容器、特定方块互动。 红石比较器有两种模式: 比…...
危化品经营单位安全生产管理人员考试主要内容
危化品经营单位安全生产人员考试主要测试从业人员对危险化学品安全管理的专业知识和法规掌握程度。考试内容涵盖以下重点: 法律法规(30%) 重点考查《安全生产法》《危险化学品安全管理条例》等核心法规,以及经营许可、重大危险源…...
get_the_category() 和 get_the_terms() 的区别
get_the_category() 和 get_the_terms() 是WordPress中用于获取文章分类的两个函数,但它们之间存在一些关键差异: get_the_category() 特定于分类:get_the_category() 函数专门用于获取文章的分类(category)。它返回一个包含所有分类对象的…...

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

豪越科技:消防应急装备智能仓储管理新变革
在消防救援工作中,消防装备无疑是消防员们与火灾等灾害顽强对抗的关键“武器”。然而,传统的消防装备管理模式长期以来饱受诸多痛点的困扰,严重影响着消防工作的高效开展和救援效果。 在过去,装备丢失的情况时有发生。由于缺乏有效…...

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

毕业论文格式(Word)
目录 Word目录怎么自动生成?快速生成试试这3个方法! - 知乎https://zhuanlan.zhihu.com/p/692056836目录生成需要先设置标题样式,这个不仅是目录生成需要,和后续的图表也有关系。 最好不要自己创建新的样式,而是在现有…...

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

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

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

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

LangChain构建RAG的对话应用
目录 Langchain是什么? LangSmith是什么? 编辑 使用Python构建并使用AI大模型 数据解析器 提示模版 部署 记忆功能 Chat History -- 记忆 代码执行流程: 流式输出 构建向量数据库和检索器 检索器 代码执行流程 LLM使用检索器…...

目标检测DN-DETR(2022)详细解读
文章目录 gt labels 和gt boxes加噪query的构造attention maskIS(InStability)指标 在DAB-Detr的基础上,进一步分析了Detr收敛速度慢的原因:二分图匹配的不稳定性(也就是说它的目标在频繁地切换,特别是在训…...

嵌入式培训之系统编程(四)进程
一、进程的基本概念 (一)定义 进程是一个程序执行的过程(也可以说是正在运行的程序),会去分配内存资 源,cpu的调度,它是并发的 (二)PCB块 1、PCB是一个结构体&#x…...

天文数据处理:基于CUDA的射电望远镜图像实时去噪算法(开源FAST望远镜数据处理代码解析)
一、射电天文数据处理的挑战与CUDA加速的必要性 作为全球最大的单口径射电望远镜,中国天眼(FAST)每秒产生38GB原始观测数据,经预处理后生成数千万张图像。这些数据中蕴含的脉冲星、中性氢等天体信号常被高斯白噪声、射频干扰&…...

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

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