【Linux】:socket编程——UDP

embedded/2025/3/20 9:36:05/

朋友们、伙计们,我们又见面了,本期来给大家带来socket编程相关的知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

C 语 言 专 栏:C语言:从入门到精通

数据结构专栏:数据结构

个  人  主  页 :stackY、

C + + 专 栏   :C++

Linux 专 栏  :Linux

​ 

目录

1. 端口号

1.1 pid和port 

2. IP协议和UDP协议

3. 网络字节序 

4. socket编程 

4.1 socket 常见API

4.2 sockaddr 结构

 4.3 代码实现简易UDP通信


1. 端口号

端口号(port)是传输层协议的内容:

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用。

两个主机进行网络通信时实际上是两个进程进行通信;

所有的网络通信行为:本质都是进程间通信,但是不在一台机器上;

所以在进行通信时,对于双方而言:

① 先保证数据能到达自己这台机器(IP);

② 找到指定的进程(port)。

所以IP标识主机唯一,端口号(port)标识进程唯一。

IP和port用来标识互联网中唯一一个进程。 (ip和port合起来就叫做一组套接字)

1.1 pid和port 

我们之前学习过的进程pid不就可以用来标识进程的唯一性嘛,为什么要有port呢?

① 网络进程和port可以进行绑定关联;

② 进程管理和网络进行解耦;

③ port专门用来进行网络通信。

一个进程可以和一个或者多个端口号进行关联,但是一个端口号只能和一个进程进行关联。 

2. IP协议和UDP协议

这里只对IP和UDP进行简单认识;

IP协议:

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

UDP协议:

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

这里的可靠和不可靠不是标识的谁好谁坏,而是两个协议是不同的;

3. 网络字节序 

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

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
  • 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端,就需要先将数据转成大端; 否则就忽略,直接发送即可;

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

  • h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

4. socket编程 

4.1 socket 常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);// 发送信息
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);// 接收信息
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);

网络编程时,socket有很多类别:

  • ① unix socket:域间 socket,用同一台机器上文件路径,类似于命名管道,用于本主机内部进行通信;
  • ② 网络socket:ip + port进行网络通信;
  • ③ 原始socket:编写一些网络工具。

既然有这么多类型,所以根据不同的场景,应该给每一种场景都设计一套编程接口,但是这样做太麻烦了,所以设计者就用一套接口来设计,其中,struct sockaddr 就是一个通用的地址类型。

4.2 sockaddr 结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同;


当我们使用sockaddr时,会根据前两个字节来判别进行哪种通信!

sockaddr 结构

sockaddr_in 结构

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

in_addr结构

in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数;

 4.3 代码实现简易UDP通信

Comm.hpp

#pragma onceenum{Usage_Err = 1,Socket_Err,Bind_Err
};

Log.hpp(日志)

#pragma once#include <iostream>
#include <fstream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>enum
{Debug = 0,Info,Warning,Error,Fatal
};enum
{Screen = 10,OneFile,ClassFile
};std::string LevelToString(int level)
{switch (level){case Debug:return "Debug";case Info:return "Info";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "Unknown";}
}const int defaultstyle = Screen;
const std::string default_filename = "log.";
const std::string logdir = "log";class Log
{
public:Log() : style(defaultstyle), filename(default_filename){mkdir(logdir.c_str(), 0775);}void Enable(int sty) //{style = sty;}std::string TimeStampExLocalTime(){time_t currtime = time(nullptr);struct tm *curr = localtime(&currtime);char time_buffer[128];snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",curr->tm_year + 1900, curr->tm_mon + 1, curr->tm_mday,curr->tm_hour, curr->tm_min, curr->tm_sec);return time_buffer;}void WriteLogToOneFile(const std::string &logname, const std::string &message){umask(0);int fd = open(logname.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);if(fd < 0) return;write(fd, message.c_str(), message.size());close(fd);// std::ofstream out(logname);// if (!out.is_open())//     return;// out.write(message.c_str(), message.size());// out.close();}void WriteLogToClassFile(const std::string &levelstr, const std::string &message){std::string logname = logdir;logname += "/";logname += filename;logname += levelstr;WriteLogToOneFile(logname, message);}void WriteLog(const std::string &levelstr, const std::string &message){switch (style){case Screen:std::cout << message;break;case OneFile:WriteLogToClassFile("all", message);break;case ClassFile:WriteLogToClassFile(levelstr, message);break;default:break;}}void LogMessage(int level, const char *format, ...) // 类C的一个日志接口{char leftbuffer[1024];std::string levelstr = LevelToString(level);std::string currtime = TimeStampExLocalTime();std::string idstr = std::to_string(getpid());char rightbuffer[1024];va_list args; // char *, void *va_start(args, format);// args 指向了可变参数部分vsnprintf(rightbuffer, sizeof(rightbuffer), format, args);va_end(args); // args = nullptr;snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s] ",levelstr.c_str(), currtime.c_str(), idstr.c_str());std::string loginfo = leftbuffer;loginfo += rightbuffer;WriteLog(levelstr, loginfo);}// void operator()(int level, const char *format, ...)// {//     LogMessage(int level, const char *format, ...)// }~Log() {}private:int style;std::string filename;
};Log lg;class Conf
{
public:Conf(){lg.Enable(Screen);}~Conf(){}
};Conf conf;

nocopy.hpp

// 禁止拷贝
#pragma once#include <iostream>class nocopy
{public:nocopy(){}nocopy(const nocopy &) = delete;const nocopy& operator = (const nocopy &) = delete;~nocopy(){}};

Main.cc

#include "UdpServer.hpp"
#include "Comm.hpp"
#include <memory>void Usage(std::string proc)
{std::cout << "Usage : \n\t" << proc << "local_ip local_port\n" << std::endl;
}int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);return Usage_Err;}// 获取填入的ip和portconst std::string ip = argv[1];const uint16_t port = std::stoi(argv[2]);// 构造UdpServer对象std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip,port);usvr->Init();usvr->Start();return 0;
}

UdpServer.hpp

#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "nocopy.hpp"
#include "Log.hpp"
#include "Comm.hpp"const static uint16_t defaultport = 8080;
const static int defaultfd = -1;
const static int defaultsize = 1024;class UdpServer : public nocopy
{
public:UdpServer(const std::string &ip, uint16_t port = defaultport):_ip(ip), _port(port), _sockfd(defaultfd){}void Init(){// 1. 创建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){lg.LogMessage(Fatal, "socket errr, %d : %s\n", errno, strerror(errno));exit(Socket_Err);}lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd);// 2. 绑定--指定网络信息// 设置结构体struct sockaddr_in local;bzero(&local, sizeof(local)); // memsetlocal.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_ip.c_str());// 绑定至内核int n = ::bind(_sockfd,(struct sockaddr*)&local,sizeof(local));if(n != 0){lg.LogMessage(Fatal, "bind errr, %d : %s\n", errno, strerror(errno));exit(Bind_Err);}}void Start(){// 服务器启动了就不能停止了char buffer[defaultsize];for(;;){struct sockaddr_in peer;socklen_t len = sizeof(peer);// 接收消息ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&peer,&len);if(n > 0){// 接收成功buffer[n] = 0;std::cout << "client say# " << buffer << std::endl;// 发送消息sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);}}}~UdpServer(){}
private:std::string _ip;uint16_t _port;int _sockfd;
};

UdpClient.cc

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cerrno>
#include <cstring>
#include <string>
#include "Log.hpp"
#include "Comm.hpp"const static int defaultsize = 1024;void Usage(std::string process)
{std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);return Usage_Err;}// 获取服务器IP和端口std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 创建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){lg.LogMessage(Fatal, "socket errr, %d : %s\n", errno, strerror(errno));exit(Socket_Err);}lg.LogMessage(Info, "socket success, sockfd: %d\n", sockfd);// 填写server端结构体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());while (true){// 输入std::string inbuffer;std::cout << "Please Enter# ";std::getline(std::cin, inbuffer);// 发消息ssize_t n = sendto(sockfd, inbuffer.c_str(), sizeof(inbuffer), 0, (struct sockaddr *)&server, sizeof(server));if (n > 0) // 发送成功{char buffer[defaultsize];// 收消息struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);if (m > 0){buffer[m] = 0;std::cout << "server echo# " << buffer << std::endl;}elsebreak;}elsebreak;}close(sockfd);return 0;
}

Makefile

.PHONY:all
all:udp_server udp_clientudp_server:Main.ccg++ -o $@ $^ -std=c++14
udp_client:UdpClient.ccg++ -o $@ $^ -std=c++14.PHONY:clean
clean:rm -f udp_server udp_client

需要注意的是:在实现client时我们填写了结构体是不需要显示的bind,client会在首次发送数据的时候会自动进行bind,因为server端的端口号是众所周知的,不可改变的,client端不一定只有一个,有可能有很多个client同时链接server端,所以client 需要bind,但是不需要显示bind,让本地OS自动随机bind,选择随机端口号。 

我们可以在此基础上更新出另外两个版本:

① 通过udp通信实现一个远程执行命令的版本:https://gitee.com/yue-sir-bit/linux/tree/master/2.udp_server_excute

② 通过udp于线程池实现一个远程聊天室版本 

https://gitee.com/yue-sir-bit/linux/tree/master/3.udp_server_chat


http://www.ppmy.cn/embedded/174104.html

相关文章

【面试场景题-Redis中String类型和map类型的区别】

今天在面试中碰到一个场景题&#xff1a;在 Redis 中存储 100 万用户数据时&#xff0c;使用 String 类型和 Hash&#xff08;Map&#xff09;类型的主要区别是什么&#xff1f;体现在以下几个方面&#xff1a; 1. 存储结构与内存占用 String 类型 存储方式&#xff1a;每个用…

【2025】基于Springboot + vue实现的毕业设计选题系统

项目描述 本系统包含管理员、学生、教师三个角色。 管理员角色&#xff1a; 用户管理&#xff1a;管理系统中所有用户的信息&#xff0c;包括添加、删除和修改用户。 配置管理&#xff1a;管理系统配置参数&#xff0c;如上传图片的路径等。 权限管理&#xff1a;分配和管理…

Redis GeoHash 详解

Redis GeoHash 详解 Redis 提供了 Geo&#xff08;地理位置&#xff09; 模块&#xff0c;其中 GeoHash 是一种用于存储和查询地理位置信息的数据结构。它能够高效地进行地理位置存储、查询、计算距离和查找附近地点等操作。 1. 什么是 GeoHash&#xff1f; GeoHash 是一种将…

学习使用smartengine

1、开源地址 smartengine的地址 GitCode - 全球开发者的开源社区,开源代码托管平台 2、如何基于这个开源的框架实现自己的业务定制 参考一些文章&#xff1a; 探索BPMN—工作流技术的理论与实践&#xff5c;得物技术

「C++输入输出」笔记

参考&#xff1a;比特鹏哥 1. getchar和putchar 1.1 getchar 函数原型&#xff1a;int getchar(void) 1.1.1 头文件 <cstdio> 1.1.2 空格&#xff0c;换行都会当成字符读取 1.1.3 返回值类型为整型&#xff0c;读取失败返回E0F(-1) #include<iostream> #inc…

利用大语言模型生成的合成数据训练YOLOv12:提升商业果园苹果检测的精度与效率

之前小编分享过关于《YOLO11-CBAM集成&#xff1a;提升商业苹果园树干与树枝分割的精准度》&#xff0c;改进YOLO11算法后&#xff0c;进行苹果树的实例分割。本期文章我们将分享关于最新的YOLO12算法改进的苹果目标检测。 论文题目&#xff1a;Improved YOLOv12 with LLM-Gen…

Metasploit Framework(MSF)使用教程与命令详解

Metasploit Framework&#xff08;简称MSF&#xff09;是一款功能强大的开源渗透测试工具&#xff0c;广泛应用于网络安全领域。它集成了大量的漏洞利用模块&#xff08;exploits&#xff09;、辅助模块&#xff08;auxiliary&#xff09;和载荷&#xff08;payloads&#xff0…

k8s主要控制器简述(一)ReplicaSet与Deployment

目录 一、ReplicaSet 关键特性 示例 解释 支持的 Operator 二、Deployment 1. 声明式更新 示例 2. 滚动更新 示例 3. 回滚 示例 4. ReplicaSet 管理 示例 5. 自动恢复 示例 6. 扩展和缩容 示例 示例 一、ReplicaSet ReplicaSet 是 Kubernetes 中的一个核心控…