Linux网络编程基础API

发布时间:2026/8/19 9:40:14
Linux网络编程基础API 引言本篇文章开始就正式进入高性能服务器的代码方面了参考书目为《Linux高性能服务器编程》所有的代码我会在github上面进行提交我github的仓库地址是fengyue05/Linux-: 本仓库里面会有关于《Linux高性能服务器编程》的一些主要代码实现socket地址API不同的主机对于字节在内存中的排序是不同的。字节序分为大端字节和小端字节大端字节序是一个整数的高位字节存储在内存的低地址处而小端字节序是一个整数的高位字节序存储在内存的高位所以一个主机跟另外一个主机通信的时候如果双方的字节序不同那么传同一个数字很可能解析出不同的数据所以我们引入了网络字节序网络字节序是大端字节序我们主机发送数据都是先转变成大端字节序然后再发给对端对端再根据自己的情况进行解析。uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);socket地址我们需要理解几个概念我们会经常看到一个变量sa_family_t 这个是地址族类型的变量。地址族类型的变量通常与协议族类型对应。比如说TCP/IPv4的协议族是PF_INET地址族是AF_INET对于TCP/IPv4的协议族有一个专用的socket地址结构体这个结构体里面要传入地址族端口号IPv4的地址结构体。注意这里都是要用网络字节序。IP地址转换函数我们一般人们都是用点分十进制来识别一个ip地址但是对于计算机来说都是二进制所以我们需要对两者进行转化我们有几个函数可以做到inet_addr(const char* p) inet_aton(const char* cp, struct in_addr* inp) inet_ntoa(struct in_addr in)对于前两个函数都是转化为点分十进制唯一的区别是第二个将转化的结果存入inp中。第三个函数是转化为二进制但是这个函数有一个缺点这个变量是一个静态变量所以整个进程里面只有一个内存来存储这个转化出来的二进制的地址也就是不可以重入所以我们不推荐这个写法。我们推荐下面的几个写法af参数指定地址族cnt表示目标存储单元的大小要确定这个是ipv4还是ipv6。inet_ntop(int ad, const void* src, char* dst, socklen_t cnt) inet_pton(int af, const char* src, void* dst)创建socketdomain表示告诉系统使用哪个底层的协议族在Linux里面协议族和地址族是一样的type表示的是服务的类型服务的类型主要有SOCK_STREAM流服务SOCK_DGRAM表示的是数据报服务。因为TCP是字节流所以是SOCK_STREAM而UDP是一个数据报一个数据报发送所以是SOCK_DGRAM。最后一个参数是协议我们一般来说有两个可以填的一个是非阻塞SOCK_NONBLOCK一个是SOCK_CLOEXEC。我想解释一下SOCK_CLOEXEC。当fork了一个子进程子进程会继承父进程的connfd和其socket这个connfd和父进程一样但是当我们在父进程里面closeconnfd的时候只会断开父进程的socket而子进程的socket不会断开。因为我们的close其实是对socket的引用计数-1而socket最后的消失条件是引用计数到0对于不同的进程里面的文件描述符都是完全不一样的所以如果有多个进程都同时拥有一个socket的fd那么只有在所有的进程里面都close才可以关闭。假设我们父进程负责listen子进程负责连接处理那说明子进程要一个独立的socket与客户端通信而不是继承父亲的那个socket所以我们需要在继承的时候关闭子进程的那个父亲的socket。而SOCK_CLOEXEC就是这个作用。int socket(int domain, int type, int protocol);命名socket我们创建socket时我们给它指定了地址族但是并未指定使用该地址族中的哪个具体socket地址。而客户端想要连接一个服务器就必须要知道服务器的地址和端口号。客户端则通常不需要命名socket操作系统会自动分配socket地址。命名操作系统就需要调用bindint bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 功能为套接字分配名称给套接字绑定ip地址和端口号监听socket创建一个监听队列来存储客户以待处理的客户连接。int listen(int sockfd, int backlog);接受连接从监听队列里面接受一个连接但是accept只是取出连接而不论连接处于什么样子的状态更不关心对方网络的情况。关闭连接我们一般来说都是close文件描述符但是close的底层其实是让socket的引用计数-1只有当引用计数0的时候socket才会被结束。那么这就会有一个非常麻烦的事情当子进程继承了父进程的socket之后那么我们需要关闭两个进程里面的fd才可以结束socket所以对于此我们提供了一个shutdown这个可以直接操控socket的行为。有三个选项int shutdown(int socket, int howto) SHUT_RD 禁用读 SHUT_WR 禁用写 SHUT_RDWR 禁用读和写关闭socket上读的那一半那么应用程序就不可以在针对socket文件描述符执行读操作并且socket接受缓冲区里面的数据都会被丢弃。关闭socket写的那一半socket发送缓冲区中数据会在真正关闭之前全部发送出去应用程序不可以再针对socket文件描述符进行写操作。TCP数据读写这里有几个可以学习的写法我们把地址转化成二进制然后传播出去我们使用inet_pton这个样子可以直接把ip的值以二进制的形式传入我们的结构体里面。我们接受数据的buffer最后一个是\0所以我们可以先开始把所有的位置都填充为\0最后接受的长度是buffer.size() - 1带外数据我们这里涉及到了一个之前没有提及过的点就是带外数据。我们可以理解为紧急数据但是由于TCP的数据报里面只留了一个字节来接受紧急数据所以我们即使是声明了abc是紧急数据但是我们标记的紧急数据只会到c的地方所以我们最后打印出紧急数据的只有c而123ab都是不同数据。#define BUF_SIZE 128 int main(int argc, char* argv[]) { if (argc 2) { return -1; } char* ip argv[1]; int port atoi(argv[2]); sockaddr_in address; bzero(address, sizeof(address)); address.sin_family AF_INET; address.sin_port htons(port); inet_pton(AF_INET, ip, address.sin_addr); int sockfd socket(AF_INET, SOCK_STREAM, 0); assert(sockfd 0); int ret bind(sockfd, (sockaddr*)address, sizeof(address)); assert(ret ! -1); ret listen(sockfd, 128); assert(ret ! -1); sockaddr_in client; socklen_t len sizeof(client); int connfd accept(sockfd, (sockaddr*)client, len); if (connfd 0) { return -1; } else { char buffer[BUF_SIZE]; memset(buffer, \0, BUF_SIZE); ret recv(connfd, buffer, BUF_SIZE - 1, 0); std::cout buffer std::endl; memset(buffer, \0, BUF_SIZE); ret recv(connfd, buffer, BUF_SIZE - 1, MSG_OOB); std::cout buffer std::endl; memset(buffer, \0, BUF_SIZE); ret recv(connfd, buffer, BUF_SIZE - 1, 0); std::cout buffer std::endl; close(connfd); } return 0; }int main(int argc, char* argv[]) { if (argc 2) { std::cout failed std::endl; return 1; } const char* ip argv[1]; int port atoi(argv[2]); sockaddr_in server_address; bzero(server_address, sizeof(server_address)); server_address.sin_family AF_INET; server_address.sin_port htons(port); // 转化成二进制把ip的值填入server_address.sin_addr结构体地址里面 inet_pton(AF_INET, ip, server_address.sin_addr); int sockfd socket(AF_INET, SOCK_STREAM, 0); assert(sockfd 0); if (connect(sockfd, (sockaddr*)server_address, sizeof(server_address))) { std::cout failed std::endl; } else { std::string oob_data abc; std::string normal_data 123; send(sockfd, normal_data.c_str(), sizeof(normal_data), 0); send(sockfd, oob_data.c_str(), sizeof(oob_data), MSG_OOB); send(sockfd, normal_data.c_str(), sizeof(normal_data), 0); } ::close(sockfd); return 0; }UDP数据读写因为UDP的通信没有连接的概念所有我们每一次读取数据都必须要获取发送端的socket地址通用数据读写recvmsg sendmsg 里面有一个很重要的结构体就是struct iovec这个结构体里面封装了一块内存的起始位置和长度。这个结构体的对象可以有很多个而这些数据被分散的存放在了对象内存里面我们如果读取这些数据我们叫分散读我们写这些数据叫集中写。socket选项我们这里想要解释一下level这个是指定操作的哪个协议选项比如TCPIPv4IPv6但是这里还有一个通用socket选项与协议无关SOL_SOCKET值得指出的是我们有的socket选项只有在listen系统调用前设置才有用因为在监听队列里面的的连接至少已经完后了三次握手的前两个步骤因为最少已经是在SYN_RCVD状态)这说明服务器已经接受连接上发出的TCP同步报文段而有一些选项却应该在TCP同步报文段里面设置比如说接受窗口。int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen); int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);修改接受和发送缓冲区大小我们设置接受缓冲区和发送缓冲区都是有默认大小的如果设置的这个值小于这个默认值将不会被接受。而且我们设置的值在TCP缓冲区里面将会被系统增加1倍所以我们最后的缓冲区的大小会比我们想象的要大#define BUFFER_SIZE 512 int main(int argc, char* argv[]) { if (argc 2) { return -1; } const char* ip argv[1]; int port atoi(argv[2]); sockaddr_in server_address; bzero(server_address, sizeof(server_address)); server_address.sin_family AF_INET; server_address.sin_port htons(port); inet_pton(AF_INET, ip, server_address.sin_addr); int sockfd socket(AF_INET, SOCK_STREAM, 0); assert(sockfd 0); int sendbuf atoi(argv[3]); int len sizeof(sendbuf); // 先设置TCP发送缓冲区的大小然后立即读取这个sendbuf传递的就是我们想要修改的内存大小 setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, sendbuf, sizeof(sendbuf)); getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, sendbuf, (socklen_t*)len); if (connect(sockfd, (sockaddr*) server_address, sizeof(server_address)) ! -1) { char buffer[BUFFER_SIZE]; memset(buffer, a, sizeof(buffer)); send(sockfd, buffer, BUFFER_SIZE, 0); } close(sockfd); return 0; }在书中还有一个可以加深我们理解的场景当服务器接受窗口的大小是0服务器给客户端发送了一个确认报文并且标记其接受窗口的大小是0这个样子客户端就会暂时不发送信息了。当过了一会服务器又给客户端发送了一个重复的确认报文通知客户端已经扩大了接受窗口。总结本篇文章到这里就结束了这篇文章不是全部的书中内容只是提取了一些比较关键的内容也是基于我对于服务器开发的一些个人理解希望可以帮助大家理解~~~