Linux 网络编程笔记(2)——socket 编程

来源:互联网 发布:qq旋风 知乎 编辑:程序博客网 时间:2024/05/16 05:11
  • socket 可以看作是用户进程与内核网络协议栈的编程接口
  • 既可以用于本机的进程间通信,还可以用于网络上不同主机的进程间通信
  • listenfd 是被动套接字,可以用来接受连接。
  • accept 从已完成连接队列中取出一个队列头,得到一个新套接字 connfd。
  • connfd 是主动套接字,不能用来接受连接。
  • 服务端和客户端的 ip 和 port 四元组表示 一个连接。
  • REUSEADDR 可以解决“关闭一个服务器后,由于 TIME_WAIT 无法马上重启服务器的问题”。在 TIME_WAIT 还没消失的时候允许重启。

流协议与粘包问题

  • tcp 是字节流协议,无边界。
    • 接收方不能保证读操作的时候能返回多少个字节。
  • UDP 是基于消息的传输服务,传输的是报文,有边界。

    • 能保证接收方每次返回一条消息。
  • 几种产生可能性

    • 应用进程缓冲区的数据拷贝到套接口发送缓冲区时,如果应用层缓冲区一条消息大小超过套接口发送缓冲区的大小时,就有可能产生粘包问题。(消息被分割)
    • TCP 传输的段有最大限制(MSS),超过 MSS 就会被分割 。
    • 链路层也有最大传输限制,在 IP 网络层会分组。
    • TCP 流量控制、拥塞控制等。
    • TCP 延迟发送机制。

粘包解决方案

  • 本质上是要在应用层维护消息与消息的边界。

    • 定长包
    • 包尾加上 \r\n (ftp)(要考虑消息本身具有 \r\n 这种情况)
    • 包头加上包体长度
    • 更复杂的应用层协议
  • readn()writen()的封装,封装后只有接收到指定字节数才会跳出循环,可用于发送定长包(比方说不管数据多长,一律发送 1024 个字节,有可能浪费网络资源)。

ssize_t readn(int fd, void *buf, size_t count){    size_t nleft = count;    ssize_t nread;    char *bufp = (char*)buf;    while (nleft > 0)    {        if (nread = read(fd, bufp, nleft) < 0)        {            if (errno = EINTR) // 被中断                continue;            else                return -1;   // 其他错误        }        else if (nread == 0)        {            return count - nread; // 结束,返回读取到的字节数        }        else        {            bufp += nread;            nleft -= nread;        }    }    return count;}ssize_t writen(int fd, const char *buf, size_t count){    size_t nleft = count;    ssize_t nwritten;    char *bufp = (char*)buf;    while (nleft > 0)    {        if (nwritten = write(fd, bufp, nleft) < 0)        {            if (errno = EINTR)                continue;            else                return -1;        }        else if (nwritten == 0)            continue;        else        {            bufp += nwritten;            nleft -= nwritten;        }    }}
  • 可以自定义一种协议,包头 4 个字节表示包体长度(作为定长包的长度),后面的字节作为包体。
struct packet{    int len;    chat buf[1024]; }//...struct packet sendbuf;//...struct packet recvbuf;

发送前,先用 n = strlen(sendbuf.buf); sendbuf.len = htol(n) 填充 sendbuf.len(要注意字节序的问题)
然后再 writen(sock, sendbuf, 4+n)n 为包体长度。

MSG_PEEK 读取数据但不清除缓存

read_peek()封装了 MSG_PEEK

ssize_t recv_peek(int sockfd, void *buf, size_t len){    while(1)    {        int ret = recv(sockfd, buf, len, MSG_PEEK);        if(ret == -1 && errno == EINTR)            continue;        else            return ret;    }}

read_peek()read_n()实现

  • readline():先 read_peek()看有没有’\n’,有的话看在第几个,然后只读到这个字节。
ssize_t readline(int socket, void *buf, size_t maxline){    int ret;    int nread;    char *bufp;    int nleft = maxline;    while(1)    {        ret = recv_peek(socket, bufp, nleft); // 偷窥数据但不从缓冲区清除        // ret 为缓冲区中的数据量        if(ret < 0)            return ret;        else if(ret == 0)            return ret;        nread = ret;        for(int i = 0; i < nread; ++i)        {            if(bufp[i] == '\n')     // 有 '\n',在第 i+1 位            {                readn(socket, bufp, i+1);                if(ret != i+1)      // 没有读取到 i+1 个字节的数据                    exit(EXIT_FAILURE;)                return ret;              }        }        // 以下为没 '\n' 的处理方法        nread = ret;                    if(nread > nleft)            exit(EXIT_FAILURE);        ret = readn(socket, bufp, nread);        if(ret != nread)            exit(EXIT_FAILURE);        bufp += nread;        nleft -= nread;    }}

recv()read()的区别

  1. ssize_t read(int fd, void *buf, size_t count)
  2. ssize_t recv(int sockfd, void *buf, size_t count, int flags)
    • read()处理的 fd 可以是任何 fd, recv()的 fd 是 sockfd。
    • recv()多出来一个参数 flags 可以传入 MSG_PEEK 等参数。

僵尸进程

四种僵尸进程避免方式:

1.wait和waitpid函数 2.signal安装处理函数(交给内核处理) 3.signal忽略SIGCHLD信号(交给内核处理) 4.fork两次
  • pid_t wait(int *status)只能阻塞,一旦 wait 之后,父进程将会阻塞自己直到子进程结束运行。当我们不关心子进程的退出状态,我们可以传入空指针。
  • pid_t waitpid(pid_t pid,int *status,int options)
  • pid_t
    1. pid > 0 时,只等待进程 ID 等于 pid 的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束, waitpid就会一直等下去。
    2. pid = -1时,等待任何一个子进程退出,没有任何限制,此时waitpid 和 wait的作用一模一样。   
    3. pid = 0 时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会对它做任何理睬。
    4. pid<-1 时,等待一个指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。
  • options

    1. Linux 中只支持 WNOHANG(不挂起、非阻塞) 和 WUNTRACED
  • 可以往一个已经接收 FIN 的套接字中写,接收到 FIN 仅仅代表对方不再发送数据。

  • 在收到 RST 段之后,如果再调用 write 就会产生 SIGPIPE 信号, 对于这个信号的处理我们通常忽略即可。(对端 close 之后,如果本端再 write,第一次会产生 RST,第二次产生 SIGPIPE 信号)

    • signal(SIGPIPE, SIG_IGN);
  • 五种 I/O 模型
    1.阻塞 I/O
    2.非阻塞I/O
    3.I/O多路复用
    4.信号驱动I/O(得到信号时,仅仅表明有数据来,应用程序还要recv)
    5.异步I/O(用户得到信号时,内核已经把数据推到了用户空间)

  • 信号是异步处理的一种方式。

UDP

  • 无连接
  • 基于消息的数据传输服务
  • 不可靠
  • 一般情况 UDP 更高效
  • UDP 不需要 bind,而是再第一次 sendto 和 recvfrom 的时候自动绑定