Unix 网络编程

通用套接字地址结构:sockaddr,几种具体实现为sockaddr_in和sockaddr_in6

表示和数值之间的转换:inet_ntop/inet_pton "213.0.99.98" <-> 0x626300d5

服务器:socket->bind->listen->accept------------>read->write------>read->close

                                                        |                    |                        |

客户机:socket------------------->connect->write----------->read->close

服务器启动时没有任何报文主动发送出来。当server调用accept之后,将阻塞并等待client连接。当client调用connect进行连接时,client主动发起三次握手。当client/server向socket描述符写数据时,发送一个PUSH报文。当client调用close关闭套接字描述符时,发送四次挥手。

如果子进程先于父进程退出,而父进程又没有调用wait/waitpid,则子进程会成为僵死进程。僵死进程的一个问题是占用的系统资源没有被及时清理。可以通过wait系列函数或者处理SIGCHLD来清理僵死进程。僵死进程不占用内存,但是会占用进程id和一个进程描述符项,父进程通过wait可以获取子进程的退出状态。如果父进程对子进程的退出状态不感兴趣,可以设置SIGCHLD为SIG_IGN来忽略子进程结束信号(这种方法不具有可移植性,posix规定的可移植的方法是,注册SIGCHLD的处理函数,并wait子进程)。

//回射服务器,服务端代码
int main() {

    struct sockaddr_in server_sock, client_sock;
    server_sock.sin_family = AF_INET;
    server_sock.sin_addr.s_addr = inet_addr("0.0.0.0");
    server_sock.sin_port = htons(8888);


    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        char *err_str = strerror(sockfd);
        printf("failed to create socket, error: %s\n", err_str);
        return sockfd;
    }
    int err = bind(sockfd, (struct sockaddr *) &server_sock, sizeof(server_sock));
    if (err < 0) {
        char *err_str = strerror(err);
        printf("failed to bind socket, error: %s\n", err_str);
        return err;
    }
    err = listen(sockfd, 3);
    if (err < 0) {
        char *err_str = strerror(err);
        printf("failed to listen socket, error: %s\n", err_str);
        return err;
    }

    for (;;) {
        socklen_t client_sock_len = sizeof(client_sock);
        int conn_fd = accept(sockfd, (struct sockaddr *) &client_sock, &client_sock_len);
        if (conn_fd < 0) {
            char *err_str = strerror(conn_fd);
            printf("failed to accept socket, error: %s\n", err_str);
            return conn_fd;
        }
        pid_t pid = fork();

        if (pid == 0) {
            // child
            printf("child pid is: %d\n", getpid());
            char buf[200];
            // 也可以直接使用无缓冲io直接调用read/write,这里就是为了作
            FILE *f = fdopen(conn_fd, "w+");
            fgets(buf, 200, f);

            fputs(buf, f);
            /* 进程关闭时,系统会关闭以下资源
            fflush(f);
            fclose(f);
            close(conn_fd);
            close(sockfd);*/

            // 正确结束子进程
            exit(0);
        } else {
            close(conn_fd);
            int status;
            // 等待子进程以避免成为僵尸进程
            pid = waitpid(pid, &status, 0);
            printf("return code is: %d\n", pid);
        }

    }
}

 

/* 更健壮的服务器端程序mac实现,对于linux有些方法类型不完全相同。
1. 结束子进程时,处理SIGCHLD信号,处理由于信号不排队造成的信号丢失;另外一种方式是忽略SIGCHLD,子进程结束时不会变成僵尸进程,但是不同系统行为不同,不可靠;
2. 设置信号时,指定accept的终端发生时内核重启该调用;
3. 为了完整,依然对accept的errno判断是不是中断导致的;
*/

#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <zconf.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>

void handler(int signal) {
    printf("[handler] signal is %d\n", signal);
    pid_t pid;
    int stat;
    struct rusage usage;
    while ((pid = wait4(-1, &stat, WNOHANG, &usage)) > 0) {
        printf("[handler] child pid is: %d exit?: %d status: %d\n", pid, WIFEXITED(stat), WEXITSTATUS(stat));
        printf("[handler] usage: %d %d %d %d\n", usage.ru_stime, usage.ru_utime, usage.ru_ixrss, usage.ru_isrss);
    }
}

int main() {

    struct sigaction oact, act;
    bzero(&oact, sizeof(oact));
    bzero(&act, sizeof(act));
    act.__sigaction_u.__sa_handler = handler;
    act.sa_flags |= SA_RESTART;
    int err;
    err = sigaction(SIGCHLD, &act, &oact);
    printf("[sigaction] err is %d, and oact.sa_mask: %x oact.sa_flags: %x\n", err, oact.sa_mask, oact.sa_flags);
    struct sockaddr_in server, client;

    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = htonl(INADDR_ANY);
    server.sin_port = htons(9876);

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

    err = bind(listenfd, (struct sockaddr *) &server, sizeof(server));
    printf("[bind] err is %d\n", err);
    err = listen(listenfd, 5);
    printf("[listen] err is %d\n", err);

    for (;;) {
        int len = sizeof(client);
        int connfd = accept(listenfd, (struct sockaddr *) &client, &len);
        printf("[accept] connfd is %d\n", connfd);
        if (connfd <= 0 && errno == EINTR) {
            // CHLD设置了SA_RESTART,因此可以不用判断EINTR
            printf("[accept] interrupted %d\n", errno);
            continue;
        }
        pid_t pid = fork();
        if (pid < 0) {
            printf("[fork] pid is %d\n", pid);
            continue;
        } else if (pid == 0) {
            // child
            close(listenfd);
            printf("[fork] pid is %d\n", getpid());
            for (;;) {
                char buf[40];
                int n = read(connfd, buf, 40);
                if (n <= 0) {
                    printf("[read] data len %d\n", n);
                    break;
                }
                write(connfd, buf, n);
            }
            exit(0);
        }
        close(connfd);
    }
    return 0;
}

// 客户端程序
int main() {
    struct sockaddr_in sock_client;
    bzero(&sock_client, sizeof(sock_client));
    sock_client.sin_family = AF_INET;
    char *addr_p = "127.0.0.1";
    in_addr_t addr_n;
    inet_pton(AF_INET, addr_p, &addr_n);
    sock_client.sin_addr.s_addr = addr_n;
    sock_client.sin_port = htons(8888);

    int fd = socket(AF_INET, SOCK_STREAM, 0);
    int err = connect(fd, (struct sockaddr *) &sock_client, sizeof(sock_client));
    if (err < 0) {
        printf("err occured when connecting... %s\n", strerror(err));
        return err;
    }
    char buf[200];
    //fgets(buf, 200, stdin);// with buffer: readline
    read(STDIN_FILENO, buf, 200);
    write(fd, buf, strlen(buf) + 1);
    int dataLen = read(fd, buf, 200);
    write(STDOUT_FILENO, buf, dataLen);
    return 0;
}

---------------------------------------------------------------------

---------------------------------------------------------------------

1. IPV4套接字地址结构通常被称为网际套接字地址结构,他以sockaddr_in命名,定义在<netinet/in.h>头文件中。POSIX规范只涉及到其中的sin_family,sin_addr,sin_port。地址族为AF_INET

2. POSIX的数据类型定义在sys/types.h中

3. 通用套接字地址结构sockaddr。套接字地址结构除了要支持ipv4套接字,还得支持其他不同的协议族。为了能支持所有协议族,套接字地址是以指针的形式进行传递的,为了统一结构,定义了sockaddr,使用任何协议族(当然包括ipv4的sockaddr_in)进行套接字编程,都要对其进行强制类型转换成sockaddr*

4. ipv6套接字sockaddr_in6定义在netinet/in.h,地址族为AF_INET6

5. 新的通用套接字地址结构sockaddr_storage(netinent/in.h)足以容纳系统支持的各种协议族

6. 从进程到内核传递套接字的函数有3个,bind connect 和sendto,这些函数的参数一个是指向套接字地址的指针,另一个是套接字的长度,这样内核就知道拷贝多少数据到套接字。从内核到进程传递套接字的函数有四个,accept,recvfrom,getsockname和getpeername。这些方法中的两个参数一个是指向套接字的指针,另外一个是套接字的大小。其中大小采用值-结果传递。即,在调用方法是,进程告诉内核套接字大小,在拷贝数据时不要越界;在返回是,内核告诉进程实际拷贝的数据大小。当然对于,ipv4/ipv6这类大小固定的套接字,长度总是套接字的大小。

7. 字节排序函数htons,ntohs,htonl,ntohl(sys/_endian.h)

8. 字节操纵函数:  bzero,bcopy,bcmp(strings.h)

9. 地址转换函数inet_aton,inet_addr,inet_ntoa,实现ascii字符串和网络字节序二进制的转换。他们适用于ipv4。inet_pton和inet_ntop使用于ipv4和ipv6。p 是presentation,n不是net而是number。sock_ntop解决了协议族不同需要使用不同编程代码的问题。类似于sockaddr结构。

-------------------------------------------------------------------------------------

研究以下方法的行为,一定要有tcpdump作为分析工具。对于tcpdump使用有疑问,可以私信我。

1. socket 方法

返回一个文件描述符。第一个参数是family,可以接受AF_或者PF_开头的family,AF_是地址族,PF_是协议族,历史上曾经想单个协议族可以支持多个地址族,AF_用于创建套接字,PF_用于套接字地址结构。然而这样的想法从来没有实现过,在sys/socket.h中,两者是相等的。

2. connect 方法

如果ip地址可以找到,但是端口没有监听,则直接返回reset,错误码是ECONNREFUSED;如果是一个不存在的局域网地址,则客户端会首先使用arp协议寻找ip对应的mac地址,如果找不到,则会一直请求arp协议;如果是一个不存在的广域网地址,则会进行Sync重试,首先每隔一秒尝试6次,包括第一次;然后指数退避,倍数为2,指导达到32s重试如果失败,则返回ETIMEOUT。退避策略和系统实现有关。具体错误可以参见connect文档。重试要分清是tcp协议栈的重试还是应用程序的重试,当端口未监听时,使用telnet进行连接,通过tcpdump发现有两次握手请求和reset,但是实际上这是telnet做的重试,而不是tcp协议栈。只有使用原生协议栈观察到的抓包才是准确的。一如之前的write方法,使用vim编辑文件,发现如果插入一个字符,后面的字符全都被重写,但是这并不代表write方法的行为,这是vim的行为。write的行为是当在一个位置上写入字符时,发生的行为是覆盖。

3. bind方法

将套接字和地址、端口进行绑定。可以将地址和端口设置为0表示由内核决定使用哪个地址和端口。因为bind并不返回bind的结果,如果想知道内核绑定了哪个地址和端口,需要使用getsocket来获取这个值。bind的一个常见错误是EINUSE,此时可能端口处于TIME_WAIT状态。

4. listen方法

使用socket方法创建的套接字被假设为主动套接字,这种套接字使用connect发起连接。listen将其转变为被动套接字,指示内核应该接收指向该套接字的请求。调用listen之后,服务器就已经开始等待连接了,而不是accept之后。accept将已建立连接队列的队首返回给进程(可以做个测试,listen之后,sleep一段时间,在此时间内,从客户端发起若干连接。在sleep结束之后,多次调用accept,观察每个客户端和服务器socket的对应关系)。backlog从未有过正式定义,一般认为是已经建立连接和等待建立连接的队列总和。如果backlog已满,则客户端看到的和超时是相似的。backlog满的情况没有定义为返回RST是合理的,因为这只是服务端的临时性问题,只需稍加等待,就能成功建立连接,当然如果长时间都不能建立连接,则会返回超时错误。

5. accept 方法

第一个参数称为监听套接字,第二个参数称为已连接套接字。通常一个服务器只创建一个监听套接字进行监听,它在服务器的生命周期内一直存在;内核为每个连接的客户端创建一个套接字,当服务器完成对某个套接字的服务时,相应的套接字就被关闭。如果服务器监听之后,客户端建立连接,然后写入数据,然后关闭连接。在此之后服务端再调用accept会怎样?实验证明,服务器仍旧能够读取到客户端写入的数据。我的理解:当服务端调用listen之后,就把客户端和服务端的tcp协议栈打通了。客户端写数据实际上是写入到内核的tcp协议栈的缓冲中了。因此当服务器accept时,内核会把队首的tcp链接连同数据以文件描述符的形式返回给应用程序。注意到一点,accept返回的套接字需要应用程序在适当的时候进行关闭(如果不关闭,最后进程退出的时候,系统会关闭)。关闭该描述符的同时,服务端会向客户端发送断开连接请求报文(FIN)。注意在关闭时,需要将父子进程中对此文件描述符都关闭,使得计数器变为0,才会真正关闭。当描述符被关闭的时候,read/write已经不能再作用于该描述符,但是tcp会尝试发送排队等待发送到对端的数据,发送完之后才是终止序列。只有当描述符的引用计数为0,才会发送终止序列,如果想直接发送FIN,可以调用shutdown()代替close()。

如果服务端调用listen之后一直不调用accept会怎么样?答案:客户端连接达到backlog设置的值之后,新到来的连接会表现为timeout失败,不管之前连接的客户端有没有和服务端断开。原因是每当一个客户端连接到来时,tcp的已连接队列会加长,但是因为没有调用accept,队列中的元素永远不会取走,所以队列会一直变长直到超过backlog。

connect和accept的细节,当客户调用connect,当握手的第二个分节接收到时(server->client),connect就返回了,而当服务端接收到三次握手的第三个分节时,accept才返回。

异常情况:1.如果服务器异常终止会怎样?

客户端代码大部分时间都阻塞在从stdin读取数据上,如果服务端终止,则服务端会想客户端发送FIN,但是由于客户端阻塞在stdin上,无法接到服务器终止事件。当下次客户端向服务端发送数据时,由于服务端已经终止,会从服务端收到一个RST(相当于端口没有监听)。向已经收到RST的socket上写数据,会产生一个SIGPIPE信号。如果没有对信号进行处理,默认处理方式是终止进程。

6. signal/sigaction

signal方法的出现很早,因此在不同平台实现差异很大,应该使用sigaction。1)信号处理函数一旦安装,便一直存在;2)信号处理函数运行期间,被递交的信号是阻塞的,安装处理函数时,由sa_mask中指定的信号也被阻塞;3)如果一个信号在阻塞期间发生多次,只有一次被递交,即不排队。SIGKILL和SIGSTOP不能被捕获。

慢系统调用,如read/accept等会阻塞,但是当中断信号,例如SIGCHLD等到来的时候,这些阻塞的慢系统调用会被中断,此时errno被置为EINTR。有的内核会默认自动重启慢系统调用,但是为了保证可移植性,应该在慢系统调用返回之后检查errno是不是中断,如果是,则重启该慢系统调用。子进程会继承父进程的信号处理函数。

int acceptfd = accept ...
if (acceptfd < 0 && errno == EINTR) { // accept成功errno也是EINTR,所以要同时判断acceptfd
    continue;
} else if(acceptfd<0){
    printf("error");
    exit(0);
}

有些内核允许配置中断后是否由内核进行重启。这是posix规范。sa_mask表示在信号处理期间,block的信号集。block意思是不处理,但是不会丢失。当然了,block期间如果该信号提交了多次,由于信号不排队,最终只会保留一个。在mac上有个奇怪的问题,当使用sigaction重新注册CHLD信号,同时屏蔽SIGUSR1(30)和SIGUSR2(31)时,SIGUSR2可以被屏蔽,但是SIGUSR1还是会导致SIGCHLD中的sleep函数中断,从而立即返回。只有当同时屏蔽了SIGINFO之后,对SIGUSR1的屏蔽才会生效。同时,即使屏蔽了所有信号,19号信号也会中断SIGCHLD的sleep。当然SIGKILL是没法被屏蔽的。

在给结构体赋值之前,一定要调用bzero将整个结构重置为0,否则行为是未定义的。可能因为没有清零,导致各种奇怪问题。

    struct sigaction act, oact;
    int err;
    act.sa_mask |= 1 << SIGUSR1;
    act.sa_mask |= 1 << SIGUSR2;
    act.sa_flags &= ~SA_RESTART;
    act.__sigaction_u.__sa_handler = sig_handler_chld;

    err = sigaction(SIGCHLD, &act, &oact);

不同平台上sigaction也不一样,比如Linux如下。在linux上没有安装ide,如何找到这些定义是个问题。通常可以从/usr/include下面开始找,在其包含相关文件中如果查找相关文件,可以使用find进行查找。支持的信号类型以及信号的数字值也不同。mac SIGCHLD=20,linux=17。

__sigaddset(&act.sa_mask, SIGUSR1);
__sigaddset(&act.sa_mask, SIGUSR2);

getsockopt方法

level的可取值SOL_SOCKET, IPPROTO_*,前者的选项名以SO_头,后者则更具有多样性,如IP_,IPV6_,MCAST_等。前者定义在<sys/socket.h>中,后者定义在<netinet/in.h>,<netinet6/in6.h>中。

select/poll

select rset 可读条件: 1)套接字接收缓冲区可读取字节数≥接收缓冲区低水位标记;2)监听套接字上可以accept的fd数目大于或等于1;3)套接字读半端关闭,此时返回为0;4)套接字上有错误,返回-1;

select wset 可写条件: 1)套接字发送缓冲区可写入字节数大于等于发送缓冲区低水位标记;或者该套接字不需要链接; 2) 连接的写半部关闭; 3) 非阻塞connect已经建立,或者以失败而告终;4)有错误等待处理;

select eset 异常条件: 存在带外数据或者仍处于带外标记;

#include <strings.h>
#include<netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <zconf.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>


int main() {

    int err;
    struct sockaddr_in server, client;

    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = htonl(INADDR_ANY);
    server.sin_port = htons(9876);

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

    err = bind(listenfd, (struct sockaddr *) &server, sizeof(server));
    printf("[bind] err is %d\n", err);
    err = listen(listenfd, 5);
    printf("[listen] err is %d\n", err);

    struct fd_set rset, wset, eset;
    bzero(&rset, sizeof(rset));
    bzero(&wset, sizeof(wset));
    bzero(&eset, sizeof(eset));

    char fds[FD_SETSIZE];
    bzero(fds, FD_SETSIZE);

    int maxfd = listenfd + 1;
    for (;;) {
        int i = 0;
        for (; i < maxfd; i++) {
            if (fds[i] != 0 || i == listenfd) {
                FD_SET(i, &rset);
            }
        }
        struct timeval timeout;
        bzero(&timeout, sizeof(timeout));
        timeout.tv_sec = 0;
        timeout.tv_usec = 0;
        printf("[pre-select] just before select, maxfd: %d\n", maxfd);
        int n = select(maxfd, &rset, 0, 0, 0);
        if (n <= 0 && errno == EINTR) {
            // CHLD设置了SA_RESTART,因此可以不用判断EINTR
            printf("[accept] interrupted %d\n", errno);
            continue;
        }
        if (FD_ISSET(listenfd, &rset)) {
            printf("[listenfd] listenfd is available\n");
            int len = sizeof(client);
            int connfd = accept(listenfd, (struct sockaddr *) &client, &len);
            printf("[accept] connfd is %d\n", connfd);
            //FD_CLR(listenfd, &rset);
            fds[connfd] = 1;
            FD_SET(connfd, &rset);
            if (maxfd <= connfd) {
                maxfd = connfd + 1;
            }
            continue;
        }
        for (i = 0; i < maxfd; i++) {
            if (fds[i] == 0 || i == listenfd) {
                continue;
            }
            printf("[check-fd] interested fd: %d\n", i);
            if (FD_ISSET(i, &rset)) {
                printf("[clientfd] client fd is ready: %d\n", i);
                char buf[40];
                int n = read(i, buf, 20);
                if (n == 0) {
                    close(i);
                    fds[i] = 0;
                    FD_CLR(i, &rset);
                    printf("[close] session ended...\n");
                } else {
                    printf("[write] will write message to console...\n");
                    write(i, buf, n);
                }
            }
        }

    }
    return 0;
}

udp通信过程

对于没有connect的UDPsendto总是成功返回,即使服务端没有启动。成功返回仅仅表示缓冲区有存放 所形成IP数据报的空间。对于TCP,write成功表示已经放到TCP缓冲区。客户的临时端口由第一次sendto绑定,并且后续不能修改,但是IP地址可以变动,例如多宿主的情况。

UDP connect:没有三次握手发生,只是检查有没有立即可知的错误,例如服务没有监听,错误的检验不是发生在connect,而是在read/recvfrom,返回ECONNREFUSED;发送数据不能使用sendto和recvfrom,而是向connect指定的服务发送数据,并从其接收数据(对于Linux不是这样,仍旧可以sendto和recvfrom,但是会校验,如果和connect绑定的ip端口不一致,会产生EINVAL错误);可以通过再次调用connect来重新指定IP和端口号或者断开连接;已连接的UDP通信效率更高。

杂项

ioctl

可选参数FIONREAD FIONBIO,还支持ARP相关选项等。

recv/send比起read/write多了一个指定flag的参数recvmsg/sendmsg可以指定更多参数,是最通用的接口。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值