Linux_Socket_浅谈UDP
✨✨ 欢迎大家来到小伞的大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:LInux_st
小伞的主页:xiaosan_blog制作不易!点个赞吧!!谢谢喵!!
1. Socket预备知识
1.1 理解源IP地址和目的IP地址
- IP在网络中,用来标识主机的唯一性
- 注意:后面我们会讲IP 的分类,后面会详细阐述IP 的特点
数据传输的目的是给client使用的,所以当数据传输到目标主机时,会把数据交给对应进程,进行Server

1.2 认识端口号
端口号(port)是传输层协议的内容.
- 端口号是一个2字节16位的整数
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;
- IP地址+端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用
浏览器,qq,迅雷这些进程,操作系统怎么知道把数据报文传给对应的进程呢,通过端口号,每个进程拥有不同的端口号,如房间号,你打电话让前台送水,你要告诉他几楼(目标主机ip),房间号(端口号port),前台到达对应楼层,通过房间号(port)找到你的房间把水(数据报文)给你

1.2.1 端口号范围划分
- 0-1023:知名端口号,HTTP,FTP,SSH等这些广为使用的应用层协议,他们的端口号都是固定的
1024-65535:操作系统动态分配的端口号.客户端程序的端口号,就是由操作系统从这个范围分配的.
1.3 理解socket
- IP地址用来标识互联网中唯一的一台主机,port用来标识该主机上唯一的一个网络进程
- IP+Port 就能表示互联网中唯一的一个进程
- 所以,通信的时候,本质是两个互联网进程代表人来进行通信,{srclp,srcPort,dstlp,dstPort}(源地址+端口号 ,目的地址+端口号)这样的4元组就能标识互联网中唯二的两个进程
- 所以,网络通信的本质,也是进程间通信
- 我们把ip+port 叫做套接字socket

1.4 认识TCP 协议与UDP协议
TCP(Transmission Control Protocol 传输控制协议)
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
UDP(User DatagramProtocol用户数据报协议)
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
1.5 网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可;
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运
行,可以调用以下库函数做网络字节序和主机字节序的转换。

- 这些函数名很好记,h表示host,n表示network,I表示32位长整数,s表示16位短整数。
- 例如 htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
1.6 socket常见API
这里介绍一下,后续会讲解
C /
/ 创建 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);
// 开始监听 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);
1.7 sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及
后面要讲的UNIX Domain Socket.然而,各种网络协议的地址格式并不相同.
由于存在本地连接和网络连接,存在不同的结构体,为了适配这两种连接方式,就使用struct sockaddr强转获取

- IPv4 和 IPv6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构体表示,包括16 位地址类型,16 位端口号和32位IP 地址.
- IPv4、IPv6 地址类型分别定义为常数AF_INET、AF_INET6.这样,只要取得某种 sockaddr 结构体的首地址,不需要知道具体是哪种类型的 sockaddr 结构体,就可以根据地址类型字段确定结构体中的内容.
- socketAPl可以都用struct sockaddr*类型表示,在使用的时候需要强制转化成sockaddr_in;这样的好处是程序的通用性,可以接收IPv4,IPv6,以及UNIXDomainSocket各种类型的 sockaddr结构体指针做为参数;
1.7.1 sockaddr结构

1.7.2 sockaddr_in结构

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

in_addr用来表示一个IPv4的IP地址.其实就是一个32位的整数;
2.Socket编程UDP
// struct sockaddr_in// {// __SOCKADDR_COMMON(sin_);// in_port_t sin_port; // 端口号// struct in_addr sin_addr; // 互联网地址// unsigned char sin_zero[sizeof(struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof(in_port_t) - sizeof(struct in_addr)];// };
2.1 UdpServer.hpp
2.1.1 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket error!";exit(1);}LOG(LogLevel::INFO) << "socket success, sockfd : " << _sockfd;
创建IPv4网络,以数据报的模式读取


2.1.2 填充socket
struct sockaddr_in local;bzero(&local, sizeof(local)); // 清零local.sin_family = AF_INET;local.sin_port = htons(_port); // 网络序列为大端序列// IP也是如此,1. IP转成4字节 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp);//local.sin_addr.s_addr = inet_addr(_ip.c_str());local.sin_addr.s_addr = INADDR_ANY;//可以通过端口号连接,而不是指定IP地址
将相应的信息填充,AF_INET表示接收所有Ip地址的,将起设为0x0000000地址,等待客户端传输
2.1.3 绑定socket
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind error";exit(2); // 1,2以致于迅速找到报错位置}
我们创建了AF_INET, SOCK_DGRAM的套接字,将其与本地sockaddr绑定
注意:这里的bind是需要sockaddr,而不是sockaddr_in
以上三步是创建网络的基本步骤
2.1.4 接收客户端的信息处理
对于服务端来说,我们先要接收到客户端的请求,服务端进行处理请求

sockfd:套接字
//输出型参数
buf:接收缓冲区
len:buf大小
flags:标志符(目前设置为0)
src_addr:(struct sockaddr)一般要强制转换
addrlen:struct sockaddr大小
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (s > 0){int peer_port = ntohs(peer.sin_port); // 从网络中拿到的!网络序列std::string peer_ip = inet_ntoa(peer.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IPbuffer[s] = 0;LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port<< "]# " << buffer;// 2. 发消息std::string echo_string = "server echo@ ";echo_string += buffer;sendto(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, len);}
2.1.5 完整代码
#pragma once#include <iostream>
#include <string>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdint.h>
#include "Log.hpp"using namespace LogModule;const int defaultfd = -1;class UdpServer
{
public://UdpServer(const std::string &ip, uint16_t port)UdpServer(uint16_t port): _sockfd(defaultfd),//_ip(ip),_port(port),_isrunning(false){}void Init(){// 创建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket error!";exit(1);}LOG(LogLevel::INFO) << "socket success, sockfd : " << _sockfd;// 2. 绑定socket信息,ip和端口// 2.1 填充sockaddr_in结构体// struct sockaddr_in// {// __SOCKADDR_COMMON(sin_);// in_port_t sin_port; // 端口号// struct in_addr sin_addr; // 互联网地址// unsigned char sin_zero[sizeof(struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof(in_port_t) - sizeof(struct in_addr)];// };struct sockaddr_in local;bzero(&local, sizeof(local)); // 清零local.sin_family = AF_INET;local.sin_port = htons(_port); // 网络序列为大端序列// IP也是如此,1. IP转成4字节 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp);//local.sin_addr.s_addr = inet_addr(_ip.c_str());local.sin_addr.s_addr = INADDR_ANY;//可以通过端口号连接,而不是指定IP地址// 绑定网络int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind error";exit(2); // 1,2以致于迅速找到报错位置}LOG(LogLevel::INFO) << "bind success, sockfd : " << _sockfd;}void Start(){_isrunning = true;while (_isrunning){char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);//服务端收消息,处理数据ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (s > 0){int peer_port = ntohs(peer.sin_port); // 从网络中拿到的!网络序列std::string peer_ip = inet_ntoa(peer.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IPbuffer[s] = 0;LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port<< "]# " << buffer;// 2. 发消息std::string echo_string = "server echo@ ";echo_string += buffer;sendto(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, len);}}}~UdpServer(){}private:int _sockfd;uint16_t _port; // 端口号//std::string _ip; // 用的是字符串风格,点分十进制bool _isrunning;
};
2.2 UdpServer.cc
#include <iostream>
#include <memory>
#include "UdpServer.hpp"// std::string defaulthandler(const std::string &message)
// {
// std::string hello = "hello, ";
// hello += message;
// return hello;
// }// ./udpserver ip + port
// ./udpserver port
int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << "ip port" << std::endl;return 1;}//std::string ip = argv[1];uint16_t port = std::stoi(argv[1]); // 字符串转整形Enable_Console_Log_Strategy(); // 开启日志std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);//智能指针usvr->Init();usvr->Start();return 0;
}
2.3 UdpClient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>// ./udpclient server_ip server_port
int main(int argc, char *argv[])
{//判断是否符合格式if (argc != 3){std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;}std::string server_ip = argv[1];uint16_t server_port = std::stoi(argv[2]); // stoi,字符串转整形// 1. 创建socketint sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;return 2;}// 绑定IP地址和端口号:服务器端需要绑定一个特定的IP地址和端口号,// 以便客户端能够找到并连接到服务器。如果服务器没有绑定IP和端口,客户端将无法知道如何连接到服务器struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());while (true){std::string input;std::cout << "Please Enter# ";std::getline(std::cin, input); // 从标准输入流读取字符串// 客户端向服务器发送信息int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));(void)n;char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);//客户端从服务器中收消息int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if(m > 0){buffer[m] = 0;std::cout << buffer << std::endl;}}return 0;
}
2.4 Makefile
.PHONY:all
all:udpclient udpserverudpclient:UdpClient.ccg++ -o $@ $^ -std=c++17 -static
udpserver:UdpServer.ccg++ -o $@ $^ -std=c++17.PHONY:clean
clean:rm -f udpclient udpserver

