[Linux Programming] Socket - 套接字

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol); // creatint a socket
// @domain
// - AF_INET
// @type
// - SOCK_STREAM / SOCK_DGRAM
// @protocal: almost NULL

#include <netinet/in.h>
struct sockaddr_in // socket address in AF_INET domain
{
    short int sin_family; /* AF_INET */
    unsigned short int sin_port; /* port */
    struct in_addr sin_addr; /* ip */
};
struct in_addr 
{
    unsigned long int s_addr;
};

在sockaddr_in中, 端口和地址, 必须按照网络字节序来维护.


int bind(int sockfd, const struct sockaddr *address, size_t address_len); // naming a socket
// @address: (sockaddr*)sockaddr_in
// @address_len: sizeof(sockaddr_in)

在address中
协议域应该和创建sockfd时使用的协议域相同

ip地址和端口均可指定可不指定, 所以将会得到下面四种情况

指定ip地址 指定端口 结果
0 0 内核选择IP地址和端口
0 1 内核选择IP地址, 进程指定端口
1 0 进程指定IP地址, 内核选择端口
1 1 进程指定IP地址和端口

不指定端口时, 内核在bind时选择一个临时端口, 但该端口并不会随着spec_addr返回(const限制), 应该用getsockname来返回协议地址
不指定IP地址时, 内核将在能够确定IP地址时(TCP连接或者收到UDP的数据包时)再确定
指定IP地址, 内核将仅为该连接时使用该IP地址的客户服务

一般会不指定IP地址, 但指定端口来为所有明确知道该端口存在服务的客户提供相应服务

bind可能会失败, 常见错误是EADDRINUSE, 表示端口被占用, 此时可以使用SO_REUSEADDRSO_REUSEPORT来解决, 前提是知道这两个选项做了什么.


int listen(int sockfd, int backlog); // creating a socket queue
// @backlog: queue length

listen将一个主动套接字转换为一个被动套接字, 表示内核应该接受指向该套接字的连接请求.

内核将为listen维护两个队列: 未完成连接队列和完成连接队列.
当客户端的一个SYN到来时, 内核在未完成连接队列中创建一个新项, 并且响应这个SYN, 回复SYN和ACK, 在此期间连接状态将一直保存在未完成连接队列中, 直到三次握手的第三个分节来自客户端的SYN和ACK响应, 此时连接建立完成, 该连接将被移至已连接队列, accept将会从已连接队列中获取队列头部的连接处理, 如果已连接队列为空, 服务进程将投入睡眠.

当一个SYN到达时, 这两个队列是满的, 那将会忽略这个SYN, 不返回任何东西, 依赖于客户端的重连机制, 等待队列存在空闲时来处理这个连接.


int accept(int listenfd, struct sockaddr *address, size_t *address_len); // accepting connections

listenfd为服务器的监听描述符, accept监听此描述符上的事件, 返回一个全新的connected_fd连接描述符.
一个服务通常只有一个listenfd, 在该服务的生命周期内一直存在, 而connected_fd可以存在多个, 在不必要的时候关闭这个描述符.
关闭listenfd意味着服务器也即将关闭.

如果不关心客户端的协议地址, 那么可以将client_addr和addrlen设置为空值.


int connect(int socket, const struct sockaddr *address, size_t address_len); // requesing connections

使用该函数将发生TCP的三次握手过程, 在连接确定成功或失败时才会返回, 意味着在此期间将会阻塞.
连接过程中, 由于客户端和服务器之间是透明的, 客户端并不知道服务器的状态, 唯一所能知道的就是服务器的套接字地址和端口, 那么在connect期间就可能会出现问题:

  1. 客户端压根就没有找到这个服务器
    这时, 客户端并不会轻易放弃连接, 并为自己设置了超时, 如果时间范围内没有连到服务器, 会休息一会, 然后进行重连, 总共耗时发生75s后放弃连接, 返回一个ETIMEDOUT错误, 表示超时.

  2. 客户端找到了服务器, 但服务器并未准备为其服务
    客户端所指定的端口, 在服务器上没有使用该端口的进程, 无法提供服务, 但服务器并不会忽略客户端的好意来访, 会告诉客户端RST信号, 此时客户端就会立刻返回ECONNREFUSED错误, 表示服务器拒绝连接.

  3. 服务器不可达
    客户端收到某刻路由器发出的'目的地不可达'错误, 此时客户端会保存这个错误, 然后按第一种方式重连. 最后实在连接不上, 将保存的这个错误以EHOSTUNREACHENETUNREACH错误返回给进程.

connect连接过程中的状态迁移:
CLOSED ---[send SYN to server]---> SYN_SEND ---[connect success]---> ESTABLISHED

若connect连接失败, sockfd将不能在被使用, 需要关闭后新建sockfd.


int close(int socket);
// close server, when read == 0

close是sockfd上的引用计数-1, 当引用计数为0时, 将对应套接字标记为关闭, 并极力返回到调用进程. 此时该描述符将不可在被使用, 而内核将发送在该套接字上已排队等待发送的数据, 发送完毕后发生正常的TCP断开连接(四次挥手).

如果需要立即关闭这个套接字, 那么应该使用shutdown函数, 见下.

int shutdown(int sockfd, int howto); // return 0/-1

close仅将连接计数-1, 在计数为0时关闭连接, shutdown则是直接关闭
close将关闭读写两个方向上的传输, 而shutdown可以通过howto来控制读写单功能上的关闭


发表评论

电子邮件地址不会被公开。必填项已用 * 标注