基本概念

网络中进程之间要通信首先需要标识自己,本地是通过PID标识的,而网络中则是通过IP地址标识主机,通过协议和端口标识进程。使用TCP/IP协议的应用程序通常采用套接字通信,套接字源于UNIX。

在网络编程中还需要注意字节序,网络字节序为大端,因此将CPU数据传递给网络接口的时候,需要进行字节序转换。常用的也就四个函数:hton[sl]()、ntoh[sl]()。几个字母的含义是Host、Network、Short、Long。

网际网协议族和OSI模型如下图所示:

osi-model.png

UDP不保证到达目的地,不保证顺序不变,不保证只达一次,不需要两端长期连接,数据报包含长度信息。

TCP需要建立连接,三次握手,基于字节流,提供确认机制,全双工。

SCTP和TCP最大区别是提供了多宿主连接,四次握手,支持多个流,基于消息流。

TCP机制

基本概念

连接三次握手

  • 服务端:socket、bind、listen被动打开
  • 客户端:connect主动打开,发送SYN,握手开始
  • 服务端:accept确认ACK并发送SYN
  • 客户端:确认服务端SYN,此时客户端connect返回,服务端accept返回

断开四次握手

  • A主动关闭:close发送FIN
  • B被动关闭:确认FIN
  • B主动关闭:close发送FIN
  • A确认关闭:确认FIN

应用程序接口

数据结构

struct sockaddr_in {
    sa_family_t    sin_family;          // e.g. AF_INET, AF_INET6
    in_port_t      sin_port;            // e.g. htons(3490)
    struct in_addr sin_addr;            // see below
};
struct in_addr {
    uint32_t       s_addr;              // load with inet_pton()
};

struct sockaddr_in6 {
    sa_family_t     sin6_family;        // address family, AF_INET6
    in_port_t       sin6_port;          // port number, Network Byte Order
    uint32_t        sin6_flowinfo;      // IPv6 flow information
    struct in6_addr sin6_addr;          // IPv6 address
    uint32_t        sin6_scope_id;      // Scope ID
};
struct in6_addr {
    unsigned char   s6_addr[16];        // load with inet_pton()
};

struct sockaddr_un {
    sa_family_t sun_family;              // AF_UNIX
    char        sun_path[UNIX_PATH_MAX]; // pathname
};

作为参数传递时要使用通用套接字sockaddr,相当于是一个通用结构。之所以这么麻烦,是因为指定标准时还没有void类型。

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

创建套接字

int socket(int domain, int type, int protocol);

参数domain指定地址类型。

AF_INET
IPv4地址加端口
AF_INET6
IPv6地址加端口
AF_UNIX
绝对路径

参数type指定套接字的类型。

SOCK_DGRAM
固定长度,无连接,不可信赖消息,使用UDP协议,无连接的含义是不需要建立连接,直接发送数据包即可
SOCK_STREAM
序列化、可信赖、双向面向连接的字节流
SOCK_RAW
IP报文接口
SOCK_SEQPACKET
固定长度、序列化、可信赖、面向连接的消息

参数protocol指定协议,包括:IPPROTO_IP、IPPROTO_IPV6、 IPPROTO_ICMP、IPPROTO_RAW、IPPROTO_TCP、IPPROTO_UDP等。

要注意类型和协议是不能随意组合的,为了避免人工错误选择,可以设置协议为0,这样会自动选择匹配的协议。

绑定套接字

int inet_pton(int af, const char *str, void *addr);
const char *inet_ntop(int af, const void *addr, char *str, socklen_t size);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

在服务启动的时候需要绑定一个已知的IP地址和端口,客户就可以利用它来连接服务器,客户端不用自己分配,在调用connect的时候系统会自动随机分配一个。

inet_ptoninet_ntop完成in_addr与字符串IP地址之间的转换。

监听和连接

int listen(int sockfd, int backlog);

参数backlog用于指定最大连接个数。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

客户端通过connect来建立连接。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数addr用于获取客户端的协议地址,addrlen是客户端协议地址长度,如果接受成功,返回内核生成的全新描述字。注意区分参数中的sockfd是监听套接字,而返回的是已连接套接字。

断开连接

int close(int sockfd);

当使用完成之后关闭即可断开连接,关闭之后就不能继续使用该描述符。实际行为是将引用计数减1,当计数为0时才会真正去关闭套接字。

int shutdown(int sockfd, int how);

注意shutdown()不会影响引用计数,只会影响行为,由参数how来控制: SHUT_RDSHUT_WRSHUT_RDWR

数据传输

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

这一组读写方法和UNIX文件读写方法是完全相同的接口,需要注意的是凡是读写都要检查返回值,返回为0的时候表示没有读写到信息,没有读写到信息也可能是套接字被关闭,返回负数的时候表示有错误发生。

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

这一组中的sendto/recvfrom可以用于未连接的报文套接字,事实上对于报文套接字也可以调用connect()函数,这样做的好处就是可以用send/recv进行收发,调用send/recv并不会改变协议类型,但是可以自动帮我们加上目标地址。

IO模型

阻塞传输

阻塞式IO处理方法是当资源没有准备好的时候一直等待。

非阻塞传输

实际上就是不断的轮询,当资源没准备好的时候直接返回错误。

复用模型

select和poll是该模型基本命令,select用于确定哪些资源已经准备好,对于准备的好的资源就可以进行传输。复用模型的好处是可以一次查询多个资源。

函数select能够监视多个套接字,告诉你哪些可以读,哪些可以写等等。

int select(int numfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

该函数也有一些局限性,就是最大可测试数目限制为FD_SETSIZE,这通常比进程可打开文件描述符小很多。

信号驱动模型

让内核在资源准备好的时候向用户空间发送一个信号,收到信号之后开始数据传输。

异步模型

异步模型就是向内核提交一个数据传输然后返回,当传输完成之后内核发送一个信号。这种方法和信号驱动模型很相似,只不过发送信号的时间推迟到传输完成而已。