2017-09-12 21:25:18 +0000   |     operating system web socket tcp ip   |   Viewed times   |    

前言

要进行网络I/O,首先要创建一个 套接字(Socket)。 套接字向程序员提供TCP/IP服务。

这篇文章主要记录一下UNIX环境下,伯克利套接字这些函数需要注意的一些点。

总览

标准伯克利套接字Berkeley Socket API提供的函数如下, socket-api

socket()函数

socket()函数用来创建一个套接字,告诉操作系统期望的 协议类型。它的函数签名如下,若成功返回的是一个非负 套接字描述符,若出错,返回-1。系统内核会为这个套接字分配资源,比如I/O的缓存区,已连接和等待连接的队列。但这些细节内核不向用户暴露。

#include <sys/socket.h>

// 若成功,返回非负描述符。若出错,则返回-1.
int socket(int family, int type, int protocol);

其中,

socket-family socket-type-protocol socket-family-type

比如,一个基于TCP/IPv4协议的套接字,初始化参数设置如下,

int sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

首先,因为是IPv4,所以family参数选AF_INET,然后TCP是字节流协议,所以type参数只支持SOCK_STREAM,最后protocol因为是TCP,就选IPPROTO_TCP

connect()函数

TCP客户端用connect()函数来建立于TCP服务器的连接。

#include <sys/socket.h>

/** 若成功返回0,若出错则返回-1. */
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

三个参数分别是,

这里的关键就是一个 套接字地址结构。用大白话说: 就是客户端要告诉系统,它请求连接的服务器的套接字地址。

其中有两个字段,

第一个关键:sockaddr地址结构

前面说了sockaddr地址结构实际就是一个 IP地址端口 的组合,用来定位一个远程服务器的地址。

struct sockaddr {
    unsigned short    sa_family;    // address family, AF_xxx
    char              sa_data[14];  // 14 bytes of protocol address
};

注意!sockaddr类型的变量可以安全地转型为sockaddr_insockaddr_in6

sockaddr_in里就可以看出来,sin_port是占2个字节的short型。sin_addr是一个占4个字节的unsigned long

// IPv4 AF_INET sockets:

struct sockaddr_in {
    short            sin_family;   // e.g. AF_INET, AF_INET6
    unsigned short   sin_port;     // e.g. htons(3490)
    struct in_addr   sin_addr;     // see struct in_addr, below
    char             sin_zero[8];  // zero this if you want to
};

struct in_addr {
    unsigned long s_addr;          // load with inet_pton()
};

因为IPv6和IPv4是兼容的,虽然sin6_addr更长,但可以通过在IPv4的地址补零对齐。

// IPv6 AF_INET6 sockets:

struct sockaddr_in6 {
    u_int16_t       sin6_family;   // address family, AF_INET6
    u_int16_t       sin6_port;     // port number, Network Byte Order
    u_int32_t       sin6_flowinfo; // IPv6 flow information
    struct in6_addr sin6_addr;     // IPv6 address
    u_int32_t       sin6_scope_id; // Scope ID
};

struct in6_addr {
    unsigned char   s6_addr[16];   // load with inet_pton()
};

第二个关键:如果是TCP协议,connect()函数触发三次握手

三次握手的细节这里不展开。只需要知道系统内核为我们完成了三次握手的过程即可。 tcp-three-way-handshake

提一下,

如果TCP客户没有收到SYN分节的响应,则返回ETIMEOUT错误。例如4.4BSD内核发送一个SYN,若无响应则等待6秒再发送一个。若仍无响应,过24秒再发送一个。总共等到75秒还没有响应,则返回错误。

按照TCP状态转换图,connect()函数导致当前套接字从CLOSED状态(该套接字从由socket()函数创建以来一直处在的状态)转移到SYN_SENT状态,若成功则再转移到ESTABLISHED状态。若失败,则该套接字不可再用,必须关闭。不可再次调用connect()函数。

bind()函数

bind()函数为套接字绑定一个 本地地址。 通常对于TCP/IP的网际协议,协议地址是32位(4个字节)的IPv4地址或128位(16个字节)的IPv6地址,与16位(2个字节)的TCP或UDP端口号组成。

#include <sys/socket.h>

/** 若成功返回0,若出错则返回-1. */
int bind(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

调用bind()函数的时候,可以指定一个端口号,一个IP地址,也可以两个都指定,也可以两个都不指定。知名应用服务器在启动的时候绑定它们知名端口号,比如HTTP服务器绑定808080端口。否则如果指定端口号为0,内核就选择一个临时端口。

至于它的三个参数,参照connect()函数。

listen()函数

listen()函数很关键,它决定套接字是公(客户端)是母(服务器)。 listen()函数只由TCP服务器调用。

#include <sys/socket.h>

/** 若成功返回0,若出错则返回-1. */
int listen(int sockfd, int backlog);

socket()函数创建一个套接字是,它被设置为一个 主动套接字,就是将调用connect()函数发起连接的客户端套接字。listen()函数把一个未连接的套接字转换成一个 被动套接字

根据TCP状态转换图,调用listen()函数,导致套接字从CLOSED状态转换到LISTEN状态。

两个参数,

这里的 “最大连接个数” 是指:未完成连接队列(incomplete connection queue) 以及 已完成连接队列(completed connection queue) 的总和不超过backlog

listen-backlog

accept()函数

accept()函数由TCP服务器调用,用于从 已完成连接队列 头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入 睡眠(阻塞)

// 若成功,返回非负描述符。若出错则返回-1.
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

关于accept()函数,关键点在于:它返回的是一个由内核自动生成的全新的已连接套接字(connected socket)。 要和它的第一个参数sockfd 监听套接字(listening socket) 描述符区分开来。

对一个服务器来说,通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在,但内核为每一个由服务器进程接受的客户连接创建一个已连接套接字(TCP三路握手已完成)。当服务器完成对该客户的服务时,响应的已连接套接字就被关闭。

附录:TCP状态转换图

tcp-status