linux: udp服务器与客户端 CS 基于ipv4的地址结构体

一. UDP服务器创建
在UDP服务器端,主要要做以下几件事:
1. 服务器进程打开套接字文件
2. 主动绑定当前主机的地址结构体, 我们这里主要使用的是ipv4的地址结构体,
作为服务器的端口号是要主动暴露的,而绑定的当前主机的ip地址则是可以当前主机的ip地址,但是一个主机可能有多张网卡(对应多个ip),所以我们的地址结构体中绑定本地的ip可以用 0,也就是表示绑定本主机的任意一个ip地址,这样当socket文件未来接受信息的时候可以接受来自本主机的所有网卡发来的信息。
3. 在用bind 主动把本机的地址结构体绑入socket文件中
type类型
4. 以上步骤作为,我们的服务器就可以开始负责,收发数据了,套接字文件的读和写都有各自的缓冲区,也就是所谓的双工的,你可以多线程的读和写。
1.1 地址结构体的初始化
ipv4地址结构体 具体可以参考我之前的文章---网络套接字编程
原生的:

既然提到了地址结构体的初始化,那么就要介绍网络套接字编程的套路,首先网络编程为了统一你使用的接口,对于sokcet文件它的创建接口只有一个统一的地址结构体:
也就是sockaddr结构体,它就是一个中间载体而已,他的大小也就尽可能让其他的地址结结构体的设计靠近它,入上面的ipv4的地址结构体.
1. 创建地址结构体,2.初始化协议族 3. 初始化端口 4. 初始化ip地址 然后在用绑入这就是本机地址结构体的绑定,
这个过程中 对于我们在网络字节序和本机字节序的转换可以参考下面的:
ip地址的点分字符串
我们通常使用的ipv4地址 如: ”192.1.1.1“ 点分字符串的类型,一共4个点分割了4个8位一共是32位的uint32_t 类型的ip地址,所以我们通常传入的ip地址大多是点分字符串的,其次我们需要把点分字符串转网络字节序,以及网络字节序转我们看见的点分字符串,显然上面的htonl() 这个接口就是表示32位的主机转网络的,我们在这个场景是用不了的,我们我们的诉求是点分字符串转网络。
所以有这么一些接口帮我封装好了,把主机序列的ip地址提前处理,然后在转网络字节序。
1.2 关于ip地址的 转换位网络字节序的系统调用介绍

// 创建地址结构体sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(_port);// _ip :string _ip : "192.1.11.1"// 1. in_addr_t inet_addr(const char *cp);addr.sin_addr.s_addr = inet_addr(_ip.c_str());// 把点分字符串 传进去// 2. int inet_aton(const char *cp, struct in_addr *inp);inet_aton(_ip.c_str(),&addr.sin_addr);// 3. int inet_pton(int af, const char *src, void *dst);inet_pton(AF_INET,_ip.c_str(),&addr.sin_addr);之前提到过关于存放ip地址的32位的其实做了一层封装的结构体,其实在我们的sockaddr_in结构体中的ipv4地址结构体是: sin_addr 内部就封装的32位的ip地址。
所以用上面的三个结果可以帮你把点分式的ip地址转为网络序的ip地址并放入地址结构体中
ip地址网络序转字符串 谈线程安全问题
第一个接口 仅仅用于ipv4的 :
char *inet_ntoa(struct in_addr in);
这是一个老接口非常常用,先谈它的用法 : 作用就是传入 sin_addr 这个变量,然后返回了一共char*字符串指向的字符指针,这里就一个问题了,这个char* 指向的字符串应该是"192.2.2.2" 这样的点分式字符串,那这个指针肯定是动态内存的,又或者静态的,因为这个字符串是在我们传入的inet_ntoa 函数内部得到的。
我们查阅文档
: The inet_ntoa() function converts the Internet host address in, given in network byte order, to a string in IPv4 dotted-decimal notation. The string is returned in
a statically allocated buffer, which subsequent calls will overwrite.所以它是静态buffer,此后你在通过它调用得到是ip字符串也是同一个静态的buffer,那么也就意味着有线程安全问题
验证: 关于这个char*得到的ip字符串的问题
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdio>int main()
{// 创建地址结构体sockaddr_in addr1;sockaddr_in addr2;addr1.sin_addr.s_addr = 0xffffff; // 32位addr2.sin_addr.s_addr = 0;char *str2 = inet_ntoa(addr2.sin_addr);char *str1 = inet_ntoa(addr1.sin_addr);printf("%p\n", str1);printf("%p\n", str2);printf("%s\n", str1);printf("%s\n", str2);return 0;
}输出结果:
首先 0x7这个地址空间应该是在进程的共享库,但是在我们调用这个函数动态库指向的静态区的 ,这个有点绕,之后我在些共享库博客时候会提及的。 总之可以理解为它是一共静态的变量,重复使用会覆盖使用。
线程安全问题:
如下代码,简单来说就是,这是一份共享资源的静态缓冲区,那么它可能会存在线程安全问题,在APUE中, 明确提出inet_ntoa不是线程安全的函数,( Advanced Programming in the UNIX Environment 的缩写,中文对应《UNIX 环境高级编程))
经过我们如下验证哈,发现没有线程问题哈,这跟具体的品牌系统有关,我这是centos7的可能加锁了,但是为了通用,为了安全其实还有一组系统调用接口更为使用广泛。
// 线程安全问题
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void *Func1(void *p)
{struct sockaddr_in *addr = (struct sockaddr_in *)p;while (1){char *ptr = inet_ntoa(addr->sin_addr);printf("addr1: %s\n", ptr);usleep(100000);}return NULL;
}
void *Func2(void *p)
{struct sockaddr_in *addr = (struct sockaddr_in *)p;while (1){char *ptr = inet_ntoa(addr->sin_addr);printf("addr2: %s\n", ptr);usleep(100000);}return NULL;
}
int main()
{pthread_t tid1 = 0;struct sockaddr_in addr1;struct sockaddr_in addr2;addr1.sin_addr.s_addr = 0;addr2.sin_addr.s_addr = 0xffffffff;pthread_create(&tid1, NULL, Func1, &addr1);pthread_t tid2 = 0;pthread_create(&tid2, NULL, Func2, &addr2);pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;
}
const char *inet_ntop(int af, const void *src,
char *dst, socklen_t size);
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);这个思路就是 要求你主动传入一共缓冲区也就是字符串地址:
af:地址族(AF_INET对应 IPv4,AF_INET6对应 IPv6);src:指向二进制网络地址的指针(IPv4 用struct in_addr*,IPv6 用struct in6_addr*);dst:存储转换后字符串的缓冲区(需用户自行分配,避免覆盖问题);size:dst缓冲区大小(IPv4 建议 ≥INET_ADDRSTRLEN(16 字节),IPv6 建议 ≥INET6_ADDRSTRLEN(46 字节))。
#include <arpa/inet.h>
#include <stdio.h>int main() {struct sockaddr_in addr = {.sin_addr.s_addr = 0xffffffff};char ip_buf[INET_ADDRSTRLEN]; // IPv4专用缓冲区宏// 传入缓冲区大小,用socklen_t接收,用法和size_t无差异if (inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf))) {printf("%s\n", ip_buf);}return 0;
}统一记忆使用ip地址转换
// 接口使用 ip地址 使用
#include <arpa/inet.h>
#include <stdio.h>int main()
{struct sockaddr_in addr;memset(&addr, 0, sizeof(addr));std::string ip = "192.1.1.1";inet_pton(AF_INET, ip.c_str(), &addr.sin_addr);char ip_buf[16]; // IPv4专用缓冲区宏// ip地址的传入// 传入缓冲区大小,用socklen_t接收,用法和size_t无差异if (inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf))){printf("%s\n", ip_buf);}return 0;
}输出:
二. udp服务读和写
首先要深刻理解,我们的网络路文件也就是socket文件,这是我们进行读写的文件,依旧理解它是一套设计在内存的文件,我们当然也能用read和write,但是对于udp这样的我们读写套接字文件的时候要指名我们要读取来自谁的地址结构体的套记者文件和获取对方的信息,所以使用得用: 偏原生的接口

这相当于read读取信息,传入套接字文件fd,
buffer: 是你要传入的buferr数组,来接受信息,如char buffer[1024]
length 对应buffer的长度
flags: 表示以什么样的形式读取: 0默认阻塞读取
restrict: 然后是传入接受对方的地址结构体信息,这样还是用的通用地址结构体记得强转
socklen_t : 记得传入这个表示你这边的地址结构体缓冲区的大小

这相当于write读取信息,传入套接字文件fd,
buffer: 是你要传入的buferr数组,来接受信息,如char buffer[1024]
length 对应buffer的长度
flags: 表示以什么样的形式读取: 0默认阻塞读取
dest_addr: 然后是传入对方的地址结构体信息,这样还是用的通用地址结构体记得强转
socklen_t : 记得传入这个表示你这边的地址结构体缓冲区的大小
返回值:
二者都是成功返回0,否则-1同时设置errno
三 . udp服务器代码
我的思路是把服务器当成一个类来写会更爽一些。如下代码,这里服务器的简单小功能就是对客户端传递过来的commad命令进行执行,然后在返回执行后的结果,然后这里的执行借用popen 就不需要自己手搓一个了。
server.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <cerrno>
#include "log.h"typedef std::function<std::string(const std::string &)> func_t;enum exiterr
{SOCK_CREATE = 1,SOCK_BIND
};class udpServer
{
public:udpServer(const std::string &ip = "0", const uint16_t port = 8080): _ip(ip),_port(port),_isrunning(false),_sock_fd(0){}void Init(){// 创建地址结构体sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(_port);// 3. int inet_pton(int af, const char *src, void *dst);inet_pton(AF_INET, _ip.c_str(), &addr.sin_addr);socklen_t server_len = sizeof(addr);_sock_fd = socket(AF_INET, SOCK_DGRAM, 0);if (_sock_fd < 0){mylog(Fatal, "socket create errno:%d strerror:%s", errno, strerror(errno));exit(SOCK_CREATE);}mylog(Info, "socket create fd:%d", _sock_fd);if (bind(_sock_fd, (sockaddr *)&addr, server_len) < 0){mylog(Fatal, "bind error\n");exit(SOCK_BIND);}mylog(Info, "bind success");}void Run(func_t func){// 运行 首先启动运行bool类型 其次 收recv 然后执行命令 再发 sendto_isrunning = false;while (_isrunning){// 创建对方的地址结构体缓冲区 以及消息缓冲区sockaddr_in client_addr;socklen_t client_addr_len = sizeof(client_addr);char buffer[1024];ssize_t n = recvfrom(_sock_fd, buffer, sizeof(buffer), 0, (sockaddr *)&client_addr, &client_addr_len);if (n < 0){mylog(Warning, "recvfrom");continue; // 这个信息不至于退出 不要读取信息即可}buffer[n] = 0;std::string command(buffer);std::string perform_str = func(command);// 发回去 给对方sendto(_sock_fd, perform_str.c_str(), perform_str.size(), 0, (sockaddr *)&client_addr, client_addr_len);}}~udpServer(){if (_sock_fd >= 0){close(_sock_fd);}}private:int _sock_fd;std::string _ip; // 本机ipuint16_t _port; // 当前进程的端口号bool _isrunning;static Log mylog;
};
Log udpServer::mylog;main.cc
#include <iostream>
#include <string>
#include "server.hpp"
#include <stdio.h>
#include<memory>void Usage(const char *proc)
{printf("Usage:%s: port\n", proc);
}std::string func(const std::string &command)
{FILE *fd = popen(command.c_str(), "r");if (fd == nullptr){return "error";}char buffer[1024];std::string ret;while (true){int n = fread(buffer, 1, 1023, fd);if (n == 0){break;}buffer[n] = 0;ret += buffer;};pclose(fd);return ret;
}int main(int argc, char *arg[])
{if (argc < 2){Usage(arg[0]);exit(1);}uint16_t port = std::stoi(arg[1]);// // 传入端口 测试 没问题// std::cout << func("ls -li") << std::endl;std::unique_ptr<udpServer> server(new udpServer("0",port));server->Init();server->Run(func);return 0;
}四. udp客户端
udp的客户端思路更为简单,首先udp协议是不连接的,数据也是数据报的,也就意味着客户端发信息,发就是了,只要有了对方的ip地址和端口那就拿着sendto就直接发,注意我这里再次强调了udp的不面向连接使得它有一个要求就是,必须要主动写地址结构体而不能像tcp一样面向连接已经在socket文件中写好了对方的地址结构体,而udp非常需要,还有就是客户端不需要自己主动绑定,因为客户端可以被千千万万个用户使用对应的端口和ip地址都是不一样的不需要自己去绑定,让系统帮你绑定即可。
4.1 实现思路
udp客户端的实现是,是
1. 创建套接字文件用于通信 socket
2. 初始化对方的地址结构体
3. 初始化消息buffer
4. 用sendto系统调用直接发信息!
客户端最好也是一共无限循环,一个是读取recvfrom 读取已经你的请求 ,另一个是发请求。
注意我这里的简单的linux客户端实现的作用主要就是发指令给服务端让服务端去执行然后返回执行结果回显给客户端。
参考代码:
#include <iostream>
#include <string>
#include "server.hpp"
#include <stdio.h>
#include <memory>void Usage(const char *proc)
{printf("Usage:%s: port\n", proc);
}std::string func(const std::string &command)
{FILE *fd = popen(command.c_str(), "r");if (fd == nullptr){return "error";}char buffer[1024];std::string ret;while (true){int n = fread(buffer, 1, 1023, fd);if (n == 0){break;}buffer[n] = 0;ret += buffer;};pclose(fd);return ret;
}int main(int argc, char *arg[])
{if (argc < 2){Usage(arg[0]);exit(1);}uint16_t port = std::stoi(arg[1]);// 传入端口 测试 没问题//std::cout << func("ls -li") << std::endl;std::unique_ptr<udpServer> server(new udpServer("0", port));server->Init();server->Run(func);return 0;
}4.2 效果
服务端:
客户端







