什么是套接字,套接字API,套接字地址?

Socket: A socket is one end-point of an inter-process communication channel. The two processes each establish their own socket.

套接字就是进程通信链路的一个端点。

套接字API通常由操作系统提供,允许应用程序控制和使用网络套接字。

套接字地址是IP地址和端口号的组合。基于这种地址,网络套接字将收到的数据包传递给相应的进程。

套接字的地址结构:

struct sockaddr_in {
    uint8_t          sin_len;
    sa_family_t     sin_family;
    in_port_t     sin_port;
    struct in_addr     sin_addr;
    char               sin_zero[8];
};

字节序的转化?

以Internet域的套接字地址结构为例,其中包含sin_port和sin_addr(一般为unsigned int类型);对于这两个字段,我们考虑下如果不进行字节序转换可能发生的情况: 对于小端字节序,如果端口为258,即0x0102,这个端口号在内存中的表现应为,低字节为0x01,高字节为0x02,如果直接不进行转换传递为一个大端字节序的主机,大端主机会把端口翻译为0x0201,也就是513。为了避免这种情况的发生,我们在设置套接字的端口时先统一将端口转换为网络字节序(大端);

对于sin_addr,存储的是unsigned int类型,我们通常使用点分十进制表示法来表示ip,这里我们在存储和处理这个字段时也需要使用如inet_aton,inet_ntoa,inet_pton及inet_ntop来进行相应的类型转换。

UNP(Unix Network Programming)中的readn(), writen(), readline()包装函数

字节流套接字上的read和write函数表现的行为不同于通常的文件IO,在socketfd上的read,write可能出现不足值(short cut)的情况,而且这些不足值并不代表错误,可能是数据未准备好。

UNP中提出了一种处理这些不足值的方法:为这些IO系统调用编写一些包装函数,由包装函数来处理这些不足值的情况,如read()的包装函数readn():

ssize_t readn(int fd, char *buf, size_t bytes)
{
    size_t nleft = bytes;
    ssize_t nread;
    char *bufp = buf;

    while (nleft) {
        nread = read(fd, bufp, nleft);
        if (nread < 0) {
            // read process was interrupted
            if (errno == EINTR) {
                continue;
            }
            return nread;
        } else if (nread == 0) {
            break;
        }
        nleft -= nread;
        bufp += nread;
    }

    return bytes - nleft;
}

从上面这段代码中可以看出,readn()的返回条件有三种:

  1. readn()读取bytes后正常返回
  2. readn()都到文件结束符后返回
  3. readn()调用的read()发生错误

同理,可以编写处理写不足值的writen()和读取一行的readline()包装函数。再看看readline()的实现:

ssize_t readline(int fd, char *buf, size_t bytes)
{
    int i;
    ssize_t nread;
    char c, *bufp = buf;

    for (i = 1; i <= bytes; i++) {
        // UNP use read() instead of readn(),
        // With readn() I don't need to check
        // errno.
        nread = readn(fd, &c, 1);
        if (nread < 0) {
            return nread;   
        } else if (nread == 0) {
            *bufp = '\0';
            return i-1;
        }
        *bufp++ = c;
        if (c == '\n')
            break;
    }
    *bufp = '\0';

    return i;
}

readline()每次从套接字中读取一个字符,判断它是不是换行符。这种做法有一个非常明显的缺点,每次read()系统调用只读取了一个字节,效率比较低。UNP中提到了一种解决办法,程序自己设置一个缓冲(在内存),每次先将数据从套接字都到缓冲区,程序再从自身的缓冲区中读取数据。

CSAPP(Computer Systems: A Programmer's Prospective)对于UNP提供的这几个IO包装函数,提出了一些问题:

  • 修改后的readline是带缓冲的,而readn是不带缓冲的,二者不能混用;
  • readline使用了静态变量,不是线程安全的

针对这两个问题,CSAPP展示了一个更完整的IO包--RIO。RIO中将缓冲区和文件描述符绑定在一起:

typedef struct {
    int     fd;
    size_t  cnt;
    char    buf[BUFFER_SIZE];
    char *  bufp;
} rio_t;

编写带缓冲的rio_read()和rio_readline()包装函数,解决了上面两种问题。

ssize_t rio_read(rio_t *rp, char *buf, size_t bytes)
{
    size_t maxread;
    ssize_t readn;

    while (rp->cnt == 0) {
        readn = read(rp->fd, rp->buf, BUFFER_SIZE);
        if (readn < 0) {
            return -1;
        } else if (readn == 0) {
            break;
        } else {
            rp->cnt = readn;
            rp->bufp = rp->buf;
        }
    }
    maxread = bytes>rp->cnt ? rp->cnt : bytes;
    bcopy(rp->bufp, buf, maxread);
    rp->cnt -= maxread;
    rp->bufp += maxread;

    return maxread;
}

套接字函数:

//#include <sys/socket.h>
int socket(int family, int type, int protocol);
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
int bind(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int socked, struct sockaddr *cliaddr; socklen_t &addrlen);

大多数套接字函数都需要一个指向套接字地址结构的指针和结构大小作为参数,这个结构大小是必要的吗?

如:int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

这个参数的大小不可以用sizeof(*serveraddr)或sizeof(struct sockaddr)来表示吗?使用connect时,套接字地址结构由进程传递给内核,serveraddr指出了该结构的地址,但是对于不同的协议镞,他们使用的结构大小可能是不相同的,需要一个参数来指明这个结构的大小。

可以为这些函数编写相应的包装函数!

包装函数自动处理这些套接字函数的返回错误。

TCP Client/Server模型示例

EchoServer:服务器在某个选定的端口上监听客户端的请求事件,如果从客户端收到请求,每次读入一行(rio_readline),然后返回给客户端(writen);

EchoClient:客户端每次由标准输入中读取一行(fgets),将其传递给服务器(writen),然后等待服务器的回传信息(rio_readline),收到信息后输出到标准输出(fputs)。

发生某些错误时会发生什么?(客户主机崩溃,客户进程崩溃,网络连接断开)

使用多线程来实现并发时,需要注意到可能产生僵死进程的问题

On Unix and Unix-like OS, a zombie process is a process has completed execution (via the exit system call) but still has an entry in the process table. The entry is needed to allow the parent process to read its child’s exit status, once the exit status is read via the wait system call, the zombie's entry is removed from the process table.

解决方法,父进程要监听SIGCHLD信号,当收到该信号时,使用wait/waitpid来读取子进程的退出状态信息。

慢系统调用accept()可能遇到的问题

如果进程阻塞在accept调用时,收到了SIGCHLD信号,此时信号会中断accept调用,进程需要对内核返回的EINTR错误进行处理。

需要注意的问题

accept的第三个参数是socklen_t *类型的:The integer referred to by addrlen initially contains the amount of space pointed to by add. On return it will contain the actual length in bytes of the address returned.

代码参见:https://github.com/liaozhenyi/network_programming/tree/master/basic_echo

参考链接:

UNIX Network Programming

Computer Systems: A Programmer's Prospective

Comments

Powered by Pelican. Booler's Adventure © 不贰 2014-2016