《Linux高性能服务器编程》学习笔记

1. 监听socket

int listen(int sockfd, int backlog);

作用:创建一个监听队列来存放待处理的客户连接(在队列中就已经处于ESTABLISHED而不用accept返回以后)。backlog参数提示内核监听队列的最大长度,如果队列长度超过backlog,则不受理新的客户连接,和客户端也会收到ECONNREFUSED的错误信息。

处于监听队列中的连接至少完成了三次握手中的两步,如果超出backlog的是SYN_RCVD,如果在backlog内则是ESTABLISHED状态。

2. 接受连接

int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);

作用:从监听队列中取出连接,而不论连接处于何种状态,如果监听队列中处于ESTABLISHED状态的连接出现网络异常或者提前退出,accept都会成功返回

3. 发起连接

int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen);

常见错误号: ECONNREFUSED, 目标端口不存在,连接被拒绝;

ETIMEDOUT: 连接超时。

4. 关闭连接

int close(int sockfd); 注意并非立即关闭一个连接,而是将fd的引用计数减1.只有当fd的引用计数为0时才真正关闭连接。多进程程序中,一次fork系统调用默认将父进程的socket引用计数加1,因此父子进程都调用close才会真正关闭连接。

如果无论如何都要立即终止连接,可以使用shutdown系统调用: int shutdown( int sockfd, int howto); 专门为网络编程设计。

howto: 

SHUT_RD: 关闭sockfd上读的这一半,应用程序不再对socket执行读操作,并且该socket接收缓冲区的数据都被丢弃;

SHUT_WR: 关闭sockfd上写的这一半,sockfd的发送缓冲区中的数据会在真正关闭连接之前全部发送出去,应用程序不能再读操作,连接处于半关闭状态;

SHUT_RDWR: 同时关闭sockfd上的读和写。

5. TCP数据读写

对文件的读写操作同样使用于socket,但是socket编程接口提供了几个专门用于socket数据读写的系统调用,增加了对数据读写的控制;

ssize_t recv( int sockfd, void* buf, size_t len, int flags);

ssize_t send( int sockfd, const void* buf, size_t len, int flags);

recv函数flags参数通常设为0. 返回实际读取到的数据长度,它可能小于我们期望的长度len,可能要多次recv;返回0意味着通信对方已经关闭连接。

flags设置MSG_OOB可以接收发送带外数据;注意只有一个字符被当作带外数据,而且接收时的数据将被带外数据截断,即带外数据前一部分和后一部分不能在同一个recv中得到。

例如发送正常数据“123”, 再发送带外数据“abc", 再发送正常数据”123“。再接收时第一次接收正常数据的话得到”123ab", 再接收带外数据得到“c",即只有字符'c'被当作带外数据,再接收正常数据时,得到“123”。

6. UDP数据读写

ssize_t recvfrom( int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);

ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);

注意:recvfrom 和 sendto 也可以用于面向连接的socket数据读写,只不过最后两个参数传入NULL即可。

7. readv和writev函数

#include <sys/uio.h>

ssize_t readv(int fd, const struct iovec* vector, int count);  --> 分散读

ssize_t writev(int fd, const struct iovec* vector, int count);   --> 聚合写

struct iovec

{

void* iov_base;

size_t iov_len;

}

8. printf族函数

int printf(const char* format, ...);

int fprintf(FILE* stream, const char* format, ...);

int dprintf(int fd, const char* format, ...);

int sprintf(char* str, const char* format, ...);

int snprintf(char* str, size_t size, const char* format, ...);  --> 在str中至多写入size个数据。

9. sendfile函数

#include <sys/sendfile.h>

ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);

在两个fd之间传输数据(完全在内核中操作), 从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,即零拷贝

in_fd是待读出内容的fd, out_fd是待写入内容的文件描述符。offset是指定从读入文件流的哪个位置开始读,如果为空则表示默认的起始位置。count是指定传输的字节数。

注:in_fd必须是一个支持类似mmap函数的文教描述符, 即必须是一个真实的文件,不能是socket和管道,而out_fd则必须是一个socket。sendfile几乎是专门为网络上传输文件而设计的。

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/sendfile.h>
#include <assert.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(int argc, char* argv[])
{
    if (argc <= 3)
    {
        printf("usage: %s, ip address, port number, filepath.\n", basename(argv[0]));
        return -1;
    }

    const char* ip = argv[1];
    const short port = atoi(argv[2]);
    const char* filepath = argv[3];

    int fd = open(filepath, O_RDWR);
    if (fd < 0)
    {
        perror("open file error.");

        exit(-1);
    }

    struct stat filestat;
    fstat(fd, &filestat);

    struct sockaddr_in address, clientAddr;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_port = htons(port);
    inet_pton(AF_INET, ip, &address.sin_addr.s_addr);

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
    assert(ret != -1);

    ret = listen(sock, 5);
    assert(ret != -1);

    bzero(&clientAddr, sizeof(clientAddr));
    socklen_t len = sizeof(clientAddr);

    int connfd = accept(sock, (struct sockaddr*)&clientAddr, &len);
    assert(connfd >= 0);

    sendfile(connfd, fd, NULL, filestat.st_size);
    close(connfd);
    close(sock);

    return 0;
}

10. splice函数  --> 零拷贝操作

ssize_t splice(int fd_in, loff_t* off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flags);

fd_in 是待读取的文件描述符,如果是管道文件,off_in必须是NULL。

注意:fd_in和fd_out必须至少有一个管道文件描述符

flags控制数据如何移动。

SPLICE_F_MOVE  -->  如果合适的话,按整页内存移动数据。这只是对内核的一个提示,不过实际没有任何效果;

SPLICE_F_NONBLOCK  -->  非阻塞的splice操作,实际要受fd的阻塞情况影响;

SPLICE_F_MORE  -->  给内核的一个提示:后续的splice调用将读取更多数据;

SPLICE_F_GIFT  -->  对splice没有效果。

可以使用splice实现高效的回射服务器

将connfd 进行splice操作,数据读到一个pipefd[1]中,再用splice将pipefd[0]读到connfd中。

11. tee函数 --> 零拷贝

#include <fcntl.h>

ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);

fd_in和fd_out必须都是管道文件描述符!!不消耗数据,因此源文件描述符上的数据还可以用于后续的读操作。

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <libgen.h>
#include <assert.h>

#define _GNU_SOURCE

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        printf("usage: %s, <file>.\n", basename(argv[0]));
        return 1;
    }

    const char* filepath = argv[1];
    
    int fd = open(filepath, O_CREAT | O_WRONLY | O_TRUNC, 0666);
    assert(fd >= 0);

    int pipefd_stdout[2];
    int ret = pipe(pipefd_stdout);
    assert(ret != -1);

    int pipefd_file[2];
    assert((ret = pipe(pipefd_file)) != -1);

    ret = splice(STDIN_FILENO, NULL, pipefd_stdout[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
    if (ret == -1)
    {
        perror("splice error.");
        exit(-1);
    }

    assert((ret = tee(pipefd_stdout[0], pipefd_file[1], 32768, SPLICE_F_NONBLOCK)) != -1);

    assert((ret = splice(pipefd_file[0], NULL, fd, NULL, 32768, SPLICE_F_MOVE | SPLICE_F_MORE)) != -1);

    assert((ret = splice(pipefd_stdout[0], NULL, STDOUT_FILENO, NULL, 32768, SPLICE_F_MOVE | SPLICE_F_MORE)) != -1);

    close(pipefd_stdout[0]);
    close(pipefd_stdout[1]);
    close(pipefd_file[0]);
    close(pipefd_file[1]);

    return 0;
}

注:为什么要用tee从一个管道拷贝数据到另一个管道,然后再写入文件中,因为数据不仅需要流入文件中,还要输出到标准输出上,而tee是不消耗数据的!

12. 统一事件源

信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路径,一种经典的信号处理方式是将处理逻辑放到程序的主循环中,信号处理函数在信号被触发时简单通知主循环程序接收到信号,并把信号值传递给主循环,主循环再根据接收到的信号值执行对应的逻辑代码。信号处理函数通常使用管道来将信号传递给主循环:信号处理函数在管道写端写入触发的信号值,主循环程序在管道读端读取信号值,主循环知道管道有数据可读的方式可以利用I/O复用监听管道的读端fd即可,这样就能把信号事件和其它I/O事件一样处理,即统一事件源。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <assert.h>
#include <fcntl.h>
#include <errno.h>
#include <vector>

#define MAX_EVENT_NUM 1024

int pipefd[2];
std::vector<int> clients;

int SetNonblocking(int fd)
{
    int oldflag = fcntl(fd, F_GETFL);
    int newflag = oldflag | O_NONBLOCK;

    fcntl(fd, F_SETFL, newflag);
    return oldflag;
}

int addfd(int epfd, int fd)
{
    epoll_event evt;
    evt.events = EPOLLIN | EPOLLET;
    evt.data.fd = fd;

    SetNonblocking(fd);
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &evt);
}

void SigHandler(int sig)
{
    int errnum = errno;
    char val = static_cast<char>(sig);
    write(pipefd[1], &val, sizeof(val));
    errno = errnum;
}

int addsig(int sig)
{
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));

    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sa.sa_handler = SigHandler;

    int ret = sigaction(sig, &sa, nullptr);
    return ret;
}

int acceptclient(int listenfd)
{
    sockaddr_in clientaddr;
    bzero(&clientaddr, sizeof(clientaddr));
    socklen_t addrlen = sizeof(clientaddr);

    int ret = accept(listenfd, (sockaddr*)&clientaddr, &addrlen);
    assert(ret >= 0);

    char addrbuf[INET_ADDRSTRLEN];
    printf("New client: %s.\n", 
            inet_ntop(AF_INET, &clientaddr.sin_addr, addrbuf, sizeof(addrbuf)));

    return ret;
}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        printf("usage: %s, ip address, port number.\n", basename(argv[0]));
        exit(1);
    }

    const char* ip = argv[1];
    const short port = static_cast<short>(atoi(argv[2]));

    sockaddr_in addr;
    bzero(&addr, sizeof(addr));

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    inet_pton(AF_INET, ip, &addr.sin_addr.s_addr);

    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    assert(listenfd >= 0);

    int ret = bind(listenfd, (sockaddr*)&addr, sizeof(addr));
    assert(ret != -1);

    assert((ret = listen(listenfd, 5)) != -1);

    int epfd = epoll_create(5);
    assert(epfd >= 0);

    addfd(epfd, listenfd);

    assert(pipe(pipefd) != -1);

    addfd(epfd, pipefd[0]);
    
    assert(addsig(SIGTERM) != -1);
    assert(addsig(SIGINT) != -1);
    assert(addsig(SIGHUP) != -1);
    assert(addsig(SIGCHLD) != -1);

    bool stopserver = false;
    epoll_event events[MAX_EVENT_NUM];
    while(!stopserver)
    {
       ret = epoll_wait(epfd, events, MAX_EVENT_NUM, -1);
       if (ret < 0 && errno != EINTR)
       {
           perror("epoll_wait error.");
           break;
       } 
       else if (ret < 0 && errno == EINTR)
       {
           continue;
       }

       for (int i = 0; i < ret; ++i)
       {
           int sockfd = events[i].data.fd;
           if (sockfd == listenfd)
           {
               int clientfd = acceptclient(listenfd);
               addfd(epfd, clientfd);
               clients.push_back(clientfd);
           }
           else if (sockfd == pipefd[0] && events[i].events & EPOLLIN)
           {
               char sigs[256];
               memset(sigs, '\0', sizeof(sigs));

               ret = read(pipefd[0], sigs, sizeof(sigs));
               for (int i = 0; i < ret; ++i)
               {
                   switch(sigs[i])
                   {
                       case SIGTERM:
                            printf("Receieved SIGTERM.\n");
                            stopserver = true;
                            break;
                        case SIGINT:
                            printf("Received SIGINT.\n");
                            stopserver = true;
                            break;
                        case SIGHUP:
                            printf("Received SIGHUP.\n");
                            break;
                        case SIGCHLD:
                            printf("Received SIGHUP.\n");
                            break;
                   }
               }
           }
       }
    }

    close(pipefd[0]);
    close(pipefd[1]);

    for (auto client : clients)
        close(client);

    close(listenfd);
    close(epfd);
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值