引言
本篇文章开始就正式进入高性能服务器的代码方面了,参考书目为《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)创建socket
domain表示告诉系统使用哪个底层的协议族(在Linux里面,协议族和地址族是一样的),type表示的是服务的类型,服务的类型主要有SOCK_STREAM流服务,SOCK_DGRAM表示的是数据报服务。因为TCP是字节流,所以是SOCK_STREAM,而UDP是一个数据报一个数据报发送,所以是SOCK_DGRAM。最后一个参数是协议,我们一般来说有两个可以填的,一个是非阻塞SOCK_NONBLOCK,一个是SOCK_CLOEXEC。
我想解释一下SOCK_CLOEXEC。当fork()了一个子进程,子进程会继承父进程的connfd和其socket,这个connfd和父进程一样,但是当我们在父进程里面close(connfd)的时候,只会断开父进程的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地址。命名操作系统就需要调用bind
int 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,这个是指定操作的哪个协议选项,比如TCP,IPv4,IPv6,但是这里还有一个通用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,这个样子客户端就会暂时不发送信息了。当过了一会,服务器又给客户端发送了一个重复的确认报文,通知客户端已经扩大了接受窗口。
总结
本篇文章到这里就结束了!!!这篇文章不是全部的书中内容,只是提取了一些比较关键的内容,也是基于我对于服务器开发的一些个人理解,希望可以帮助大家理解~~~