UNIX网络编程基础总结

前几天因为一直在看IPC(进程间通信),有几个问题去搜的时候无意中发现这些IPC方法其实以及不大常用了,最最常用的还是socket……
我是一直以为socket只会在网络上使用较多,其他不大会使用,现在发现我可能真的错了,于是打算把学IPC的精力放在巩固和加深socket上,其他IPC只做大概了解。

最基础的 TCP 套接字编程

暂时只贴注释代码

Server

/* ***********************************************************************
    > File Name: k_server.cpp
    > Author: Key
    > Mail: keyld777@gmail.com
    > Created Time: Fri 06 Apr 2018 09:50:54 PM CST
*********************************************************************** */

#include <arpa/inet.h>
#include <iostream>
#include <netinet/in.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>

#define SERV_PORT 6372
#define LINSTENDQ 1024
#define MAXLINE 4096

typedef struct sockaddr SA;

void str_echo(int);
// server
// socket bind listen accept read/write close
int main()
{
    struct sockaddr_in servaddr, cliaddr;

    int listen_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    /*
     * socket 参数: 协议族,套接字类型,协议类型
     * 协议族有 
     * AF_INET IPv4 / AF_INET6 IPv6 / AF_LOCAL Unix域协议 / AF_ROUTE 路由套接字协议 / AF_KEY 密钥协议
     * 套接字类型有
     * SOCK_STREAM 字节流 / SOCK_DGRAM 数据报 / SOCK_SEQPACKET 有序分组 / SOCK_RAW 原始套接字
     * 协议类型有
     * IPPROTO_TCP TCP / IPPROTO_UDP UDP / IPPROTO_SCTP SCTP / 0 由前两个参数默认推导的协议类型 / ...
     */
    bzero(&servaddr, sizeof servaddr);
    servaddr.sin_family = AF_INET;
    //inet_pton(AF_INET,"127.0.0.1",&servaddr.sin_addr);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    // INADDR_ANY 0.0.0.0 监听任意ip
    // htonl : host to net long 主机数转化为网络字节序
    servaddr.sin_port = SERV_PORT; // 选择 端口

    bind(listen_fd, (SA*)&servaddr, sizeof servaddr);
    /* bind 作用: 将一个本地协议地址赋予给一个套接字
     *
     * 若不调用bind,ip和端口会在 listen / connect 调用时由内核随即分配,这对服务器来说是不允许的
     */

    listen(listen_fd, LINSTENDQ);
    /* listen 函数做了两件事
     * 1. socket创建的套接字默认是主动连接的套接字,listen将其转化为被动,即监听套接字
     * 2. 设置最大连接数量 即第二参数
     *
     * 关于主动套接字和被动套接字,其明显区别就在于客户端和服务器所绑定的套接字地址结构
     * 主动套接字的ip指的是所连接的服务器的ip,端口是服务器的端口
     * 被动套接字的ip指的是所接收的客户端的ip,端口是开放的端口
     */
    puts("listen...");
    while (true) {
        socklen_t clilen = sizeof cliaddr;
        int connfd = accept(listen_fd, (SA*)&cliaddr, &clilen), child_pid;
        /* accept 参数: 套接字描述符 socket地址结构指针 及其 长度
         * accept阻塞至收到connect请求
         */
        printf("has a client connect : %s\n", inet_ntoa(cliaddr.sin_addr));
        if ((child_pid = fork()) == 0) {
            close(listen_fd);
            str_echo(connfd);
            printf("client %s disconnect.\n", inet_ntoa(cliaddr.sin_addr));
            exit(0);
        }
    }
    return 0;
}

void str_echo(int sockfd)
{
    ssize_t n;
    char buf[MAXLINE];
    do {
        while ((n = read(sockfd, buf, MAXLINE)) > 0) {
            printf("client say : %s", buf);
            buf[n - 1] = 0;
            strcat(buf, " received!\n");
            write(sockfd, buf, strlen(buf));
            bzero(buf, MAXLINE);
        }
    } while (n < 0 && errno == EINTR);
    if (n < 0)
        fputs("str_echo : read error", stdout);
}

Client

/* ***********************************************************************
    > File Name: k_client.cpp
    > Author: Key
    > Mail: keyld777@gmail.com
    > Created Time: Fri 06 Apr 2018 11:29:57 PM CST
*********************************************************************** */

#include <arpa/inet.h>
#include <iostream>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

#define SERV_PORT 6372
#define MAXLINE 4096
#define LISTENDQ 1024

typedef struct sockaddr SA;

void str_cli(FILE* fp, int sockfd);

int main(int argc, char** argv)
{
    if (argc != 2) {
        fputs("usege : k_client server_ip", stdout);
        exit(0);
    }
    struct sockaddr_in servaddr;
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&servaddr, sizeof servaddr);
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = SERV_PORT;
    inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
    // 将点分十进制 -> 网络字节序 二进制
    // 反操作 inet_ntop
    if(connect(sock_fd, (SA*)&servaddr, sizeof servaddr) < 0) {
        fputs("connect fail!",stdout);
        exit(0);
    }
    // connect 与 bind 参数一致
    str_cli(stdin, sock_fd);
    return 0;
}

void str_cli(FILE* fp, int sockfd)
{
    char sendline[MAXLINE], recvline[MAXLINE];
    while (fgets(sendline, MAXLINE, fp) != NULL) {
        //fputs(sendline, stdout);
        write(sockfd, sendline, strlen(sendline));
        if (read(sockfd, recvline, MAXLINE) == 0) {
            fputs("str_cli : server terminated prematurely", stdout);
            exit(0);
        }
        fputs(recvline, stdout);
        bzero(recvline, strlen(recvline));
    }
}

epoll 版本

eventspoll,一般来说只会用在服务器端,至少我现在没碰到过客户端的……

这里我更改的也只有服务器端,客户端不变也不贴

/* ***********************************************************************
    > File Name: k_server.cpp
    > Author: Key
    > Mail: keyld777@gmail.com
    > Created Time: Fri 06 Apr 2018 09:50:54 PM CST
*********************************************************************** */

#include <arpa/inet.h>
#include <iostream>
#include <netinet/in.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>

#define SERV_PORT 6372
#define LINSTENDQ 1024
#define MAXLINE 4096
#define EPOLL_MAX_NUM 1024

typedef struct sockaddr SA;

void str_echo(int);
// server
// socket bind listen accept read/write close
int main()
{
    struct sockaddr_in servaddr, cliaddr;
    int listen_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    bzero(&servaddr, sizeof servaddr);
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = SERV_PORT;
    bind(listen_fd, (SA*)&servaddr, sizeof servaddr);
    listen(listen_fd, LINSTENDQ);
    puts("listening...");

    int epfd;
    struct epoll_event new_event, *events;
    if ((epfd = epoll_create(EPOLL_MAX_NUM)) < 0) {
        // 创建epoll,参数为最大监听事件数
        perror("epoll create");
        close(listen_fd);
        return 1;
    }

    new_event.events = EPOLLIN;
    new_event.data.fd = listen_fd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &new_event) < 0) {
        /* 参数 epoll描述符 epoll对监听事件的操作 监听的描述符 监听事件 
         * epoll描述符 由 epoll_create 创建
         * 操作 有 EPOLL_CTL_ADD 添加监听事件 / EPOLL_CTL_MOD 修改 / EPOLL_CTL_DEL 删除
         * 监听事件 结构如下
         * struct epoll_event {
         *     __uint32_t events; 
         *     // epoll事件 有 EPOLLIN / EPOLLOUT / EPOLLPRI / EPOLLERR / EPOLLHUP / EPOLLET / EPOLLONESHOT ,后文详细解释
         *     // 常用的大概就是 EPOLLIN / EPOLLOUT / EPOLLET 菜鸟水平如此认为 
         *     epoll_data_t data; //用户传递的数据
         * }
         *
         * typedef union epoll_data {
         *    void *ptr;
         *    int fd;
         *    uint32_t u32;
         *    uint64_t u64;
         * } epoll_data_t;
         */
        perror("epoll ctl add listen_fd");
        close(epfd), close(listen_fd);
        return 1;
    }
    events = (epoll_event*)malloc(sizeof(struct epoll_event) * EPOLL_MAX_NUM);

    int client_fd;
    char buf[MAXLINE];
    while (true) {
        int active_fds_cnt = epoll_wait(epfd, events, EPOLL_MAX_NUM, -1);

        for (int i = 0; i < active_fds_cnt; i++) {
            auto& event = events[i];
            if (event.data.fd == listen_fd) {
                //此时触发的其实也是 EPOLLIN,个人理解是本地sock_fd要求被绑定
                socklen_t clilen = sizeof cliaddr;
                if ((client_fd = accept(listen_fd, (SA*)&cliaddr, &clilen)) < 0) {
                    perror("accept");
                    continue;
                }
                char ip[20];
                printf("new connection[%s:%d]\n", inet_ntop(AF_INET, &cliaddr.sin_addr, ip, sizeof(ip)), ntohs(cliaddr.sin_port));
                //ntohs 16位操作 / ntohl 32位操作
                event.events = EPOLLIN | EPOLLET;
                event.data.fd = client_fd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &event);
            } else if (event.events & EPOLLIN) {
                // 监听的套接字被(在此为客户端)写入,即 可读(在此为服务器)的时候,触发 EPOLLIN
                printf("EPOLLIN %d\n", i);
                client_fd = event.data.fd;

                int n = read(client_fd, buf, MAXLINE);
                if (n < 0) {
                    perror("read");
                    continue;
                } else if (n == 0) {
                    // 客户端关闭时,客户端被关闭时的事件仍然是 EPOLLIN (大概是本地套接字被断开)读不出任何数据即关闭
                    printf("a connection closed : %d.\n", client_fd);
                    epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &event);
                    close(client_fd);
                } else {
                    if (buf[n - 1] == '\n')
                        n--;
                    buf[n] = '\0';
                    printf("[read]: %s\n", buf);

                    event.events = EPOLLOUT;
                    event.data.fd = client_fd;
                    epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &event);
                    /* 修改或者添加 EPOLLOUT 时会立即触发一次 EPOLLOUT 在这之后的触发条件如下
                     * 1. 某次write,写满了发送缓存区,返回错误 EAGAIN
                     * 2. 客户端处理了一些数据,又可以继续写了,此时便会触发 EPOLLOUT
                     */
                }
            } else if (event.events & EPOLLOUT) {
                printf("EPOLLOUT %d\n", i);
                client_fd = event.data.fd;
                strcat(buf, " recived!");
                write(client_fd, buf, strlen(buf));
                printf("[write]: %s\n", buf);
                bzero(buf, strlen(buf));

                event.events = EPOLLIN;
                event.data.fd = client_fd;
                epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &event);
            }
        }
    }
    close(epfd);
    close(listen_fd);
    return 0;
}

名字与地址转换 版本

线程版本

关于 epoll

现代网络编程基本上都会使用epoll作为IO复用,与其说是select / poll 的优化,不如说是弥补来的好。
epoll的几个优势如下:

  1. 监听数量无限制
  2. 不需要轮询所有监听事件,主要是基于其操作系统支持的I/O事件通知机制

关于比较重要的一个是 水平触发(LT) 和 边缘触发(ET),创建epoll套接字默认是 LT。
ET触发模式意在事件只会触发一次,不论该事件你有没有进行处理,都不会再被触发事件
而LT与之相反。

LT与ET的区别可以用一句话概括:
LT模式下只要socket处于可读状态(添加EPOLLIN事件时)或可写状态(添加EPOLLOUT事件时),就会一直返回其socket。
ET模式下在第一次返回socket后,只有当socket由不可写到可写(添加EPOLLIN事件时)或由不可读到可读(添加EPOLLOUT事件时),才会返回其socket。

比如触发了 EPOLLIN事件,有100kb 数据可读,但我只读出 50 kb ,在LT模式下它仍会触发EPOLLIN事件,而ET模式下则不会了。当然缓存区的数据并不会因为没有读出而清空。

而实际上,对于一个事件的信息,我们当然必须将其处理完全,所以ET模式下在一次事件中就应当处理完所有信息。即
如果读:必须要将缓冲区的内容全部读出,即读到缓冲区空为止
如果写:一直写,知道需写的数据写完或是缓冲区满为止。

而如果是阻塞模式的花,就可能引起其他套接字饿死的情况,随意在ET模式下套接字必须设置成非阻塞模式。

以我的入门水平只能讲这么点了………………
但是如果细究起来回来很多坑点。

其他

今天太晚了,希望明天能写完……