4.1 各种套接字 api(重要)
4.1.1 socket()
用于创建一个套接字描述符, 这个描述符指明的是 tcp 还是 udp, 同时还有 ipv4 还是 ipv6
- #include <sys/socket.h>
- ?
- int socket(int family, int type, int protocol);
- // 成功返回描述符, 错误 - 1
family 主要是指明的协议族, AF_INET:ipv4AF_INET6:ipv6 AF_LOCAL:unix 域协议 AF_ROUTE: 路由套接字 AF_KEY 秘钥套接字
网络编程中主要还是前两种
type 指明套接字类型, 主要是数据报, 还是流式, 原始套接字
SOCK_STREAM: 流式, SOCK_DGRAM: 报式 SOCK_SEQPACKET 有序分组套接字 SOCK_RAW 原始套接字
protocol 是控制协议, 通常位 0, 表示由前两个参数组合出来的协议的默认类型
AF_INET | AF_INET6 | AF_LOCAL | AF_ROUTE | AF_KEY |
---|---|---|---|---|
SOCK_STREAM | TCP|SCTP | TCP|SCTP | 是 | |
SOCK_DGRAM | UDP | UDP | 是 | |
SOCK_SEQPACKET | SCTP | SCTP | 是 | |
SOCK_RAW | ipv4 自己填写 | ipv6 自己填写 | 是 | 是 |
其中 AF_和 PF_开头的都有, 但是前一个表示地址族, 后一个表示协议族但是后一个现在很少用
socket()创建的是主动套接字现在获得的套接字还不能够像普通的文件描述符一样进行读写, 套接字描述符需要绑定本端套接字 (bind) 和对端套接字(connect, 或是 send)
4.1.2 connect()
将一个套接字描述符与一对套接字地址绑定这样就使得套接字像是一个打开文件获得的文件描述符一样, 可以通过操作这个描述符来操作与一个地址之间的通信数据
- #include <sys/socket.h>
- ?
- int connect(int sfd, const struct sockaddr *sevaddr, socklen_t addrlen);
- // 成功 0, 出错 - 1, 设置 errno
sfd 是由 socket()函数获得的套接字
sockaddr *sevaddr 可以看出来, 传入的参数只能是 sockaddr 类型的, 所以需要强转
另外通过名字可以知道, 绑定的是一个服务器地址
addrlen 套接字地址结构的长度, 有了这个长度, 内核才知道, 要复制多少数据
4.1.2.1 阻塞套接字 connect
connect()通常都是客户端调用去连接服务器, 调用 connect()以后, 本机就与 sevaddr 指定的主机进行连接如果是 tcp 那么会触发三次握手, 当套接字是阻塞套接字时, 该函数仅仅在出错或是建立成功以后才会返回, 出错的情况有:
直接死在 arp 上, 返回 - 1,errno 设置 ETIMEOUT, 或是死在路由器的 arp 上是 UN**REACH
本机发送 syn, 且重发, 并等待总共 75 秒后, 没有收到 syn 分节的回应 ETIMEDOUT
本机发送 syn 分节, 收到 rst 复位分节, 表示在服务器的指定端口上, 没有进程在等待连接这是一种 硬错误, 也就是不是重试能够解决的函数返回 ECONNREFUSED
能够收到 rst 分节的情况有(这是拓展)
对应服务器的端口上, 没有进程在等待连接, 也就是没有 listen()
tcp 想要终止一条连接, 本端 EPOLLERR
tcp 收到了一条不存在连接上的数据, 也就是, 收到一条陌生的数据, 而且该数据不是 syn 分节
本机发送 syn 分节, 但是在 syn 分节在到服务器的中途中的某个 路由器上引发一个目的不可达的 icmp 错误, 是一种软错误, 可以通过重发解决本机接收到 icmp 报文以后, 重试, 如果在 75 秒内没有收到 syn 分节则返回 EHOSTUNREACH 或是 ENETUNREACH 错误
注意这里仅仅只路由器死在 arp 的时候返回的 icmp 会这样处理, 直接交付数据报死在 arp 上, 是 ETIMEDOUT
目的不可达的原因(代码默认 ipv4):
主机不可达 1, 是由路由器或是本机, 当本机要求直接交付数据(子网), 但是该主机已经离线, 死在 arp 上,(这样貌似是本机产生 icmp), 或是路由器也死在 arp 上, 那么就会发送 icmp
其中 icmpv6 中, 将直接交付产生的目的不可达, 单独作为一个代码, 0
禁止通信 3, 通常由路由器丢弃流量导致, 通常情况下, 不会产生这类报文, 防火墙直接丢弃, 不产生
端口不可达 3, 通常是 udp 中, 数据包的目的端口, 没有进程在监听返回端口不可达的 icmp
代码和禁止通信一样
数据报大于 MTU 但是设置部分片 4, 产生目的不可达 icmp
这种情况下, 该函数会:
直接返回
???
但是 ENETUNREACH 不可达已经过时了, 应该将两种错误看作一种处理
当 connect 失败的时候, 必须关闭套接字, 不能再次对同一个套接字进行 connect**
4.1.2.2 非阻塞套接字 connect
非阻塞 connect 套接字的作用:
完成一个 connect 要花费 RTT 时间, 而 RTT 波动范围很大, 从局域网上的几个毫秒甚至是广域网上的几秒, 这段时间也许有我们要执行的其他处理工作可以执行
可以使用这个技术同时建立多个连接
许多 connect 的超时实现以 75 秒为默认值, 如果应用程序想自定义一个超时时间, 就是使用非阻塞的 connect.
在一个非阻塞的套接字上调用 connect(),connect()会立即返回 EINPROGRESS 错误(非本机),0(本机), 但是已经发起的 TCP 三次握手继续进行
通常, 非阻塞的套接字, 我们不会直接去处理 connect()后的套接字, 而是在 connect()后, 将该套接字, 放入 IO 复用的 api 中
非阻塞 connect 套接字实现时需要注意的细节:
连接到同一主机上, connect 会立即完成, 我们必须处理这种情形
调用 connect, 如果返回 0, 表示连接已经完成, 如果返回 - 1, 那么期望收到的错误 errno 是 EINPROGRESS, 连接建立已经 启动, 但是尚未完成
POSIX 关于 select 和非阻塞 connect 的以下两个规则:
连接成功, 描述符会变成可写 (连接建立时, 写缓冲区空闲, 所以可写)
连接建立遇到错误时, 描述符变为可读可写 (由于有未决的错误, 从而可读又可写) 通常是 EPOLLERR 带上之前监听的事件, 严格上如果只监听了读, 那么就没有
完整的 io 复用流程位:
参考
创建非阻塞套接字, 或是, 创建以后调用 fcntl 把套接字设置为非阻塞
调用 connect, 如果返回 0, 表示连接已经完成, 如果返回 - 1, 那么期望收到的错误是 EINPROGRESS, 连接建立已经 启动, 但是尚未完成
调用 select, 并且设置超时时间
超时处理
如果 select 返回 0, 超时发生, 那么返回 ETIMEOUT 错误给调用者, 并且关闭套接字, 防止已经启动的三路握手继续下去
连接错误和成功处理
如果描述符变为可读或可写, 通过
getsockopt(sockfd,SOL_SOCKET,SO_ERROR,(char *)&error,&len)
获取的 error 值, 如果建立连接时遇到错误, 则 errno 的值是连接错误所对应的 errno 值, 比如 ECONNREFUSED,
ETIMEDOUT 等连接成功: getsockopt 返回 0 连接失败: getsockopt 返回 0, 并且获取相应的错误
muduo 使用的是该方法, 直接监听读事件, 当由 EPOLLERR 事件的时候, 直接关闭.
另一种方法是参考:
Linux 环境下是有效的:
再次调用 connect, 相应返回失败, 如果错误 errno 是 EISCONN, 表示 socket 连接已经建立, 否则认为连接失败
4.1.2.3 udp 的 connect()
默认 udp 一个套接字是无连接的, 可以向多个地址 send 数据, 但是一旦 connect()就只能向一个地址发送数据了
4.1.2.4 常见错误码
EACCES, EPERM: 用户试图在套接字广播标志没有设置的情况下连接广播地址或由于防火墙策略导致连接失败
- EADDRINUSE98:Address already in use(本地地址处于使用状态)
- EAFNOSUPPORT97:Address family not supported by protocol(参数 serv_add 中的地址非合法地址)
EAGAIN: 没有足够空闲的本地端口
- EALREADY114:Operation already in progress(套接字为非阻塞套接字, 并且原来的连接请求还未完成)
- EBADF77:File descriptor in bad state(非法的文件描述符)
- ECONNREFUSED 111:Connection refused(远程地址并没有处于监听状态)
EFAULT: 指向套接字结构体的地址非法
EINPROGRESS 115:Operation now in progress(套接字为非阻塞套接字, 且连接请求没有立即完成)
EINTR: 系统调用的执行由于捕获中断而中止
- EISCONN 106:Transport endpoint is already connected(已经连接到该套接字)
- ENETUNREACH 101:Network is unreachable(网络不可到达)
- ENOTSOCK 88:Socket operation on non-socket(文件描述符不与套接字相关)
- ETIMEDOUT 110:Connection timed out(连接超时)
- 4.1.3 bind()
常用用于服务器绑定 ip 和端口, 进行监听
一个 tcp 连接, 需要一对套接字地址结构: 对端和本端在上面的 connect 中, 我们是客户端想服务端主动发起一个连接请求, 指定了对端的套接字结构, 但是没有指定本端的, 此时内核会为我们随机选取端口号 (当然有范围要求) 和 ip 地址 (当本端有多个网卡时) 也就是说本端套接字地址结构是随机的
但是在服务器端, 服务器本端的套接字地址结构需要固定, 不然客户端怎么连接因此服务器需要显式显式的指明套接字描述符的本端套接字地址结构
bind()函数的主要作用是, 为套接字描述符绑定本端的套接字地址结构, 也就是绑定 ip 地址和端口
- #incldue <sys/socket.h>
- int bind(int sockfd, const struct sockaddr *selfaddr, socklen_t len);
- // 成功 0, 错误 - 1
sockfd 套接字描述符
sockaddr *selfad 需要强转, 这个套接字地址结构, 通常是绑定的本端的信息
客户端连接本端时候使用的 ip, 客户端连接本端使用的端口, 也就是 connect()中的套接字结构信息是一样的
len 内核复制数据需要的长度
sockaddr *selfad 各种情况:
端口
是一个需要让别人知道的端口如果不指定, 那么内核随机选, 那么你还绑定干啥?
ip 地址
ip 地址必须是本机的地址 (当存在多个网卡, 多个 ip 地址的时候需要绑定), 一旦绑定了, 那么只有 connect() 填写的 ip 地址是这个 ip 地址的时候, 数据才能被接受, 不是的就丢弃了
当该 ip 不填, 是 0 的时候, 那么所有发送到本机的数据包都能被接受后续的链接如果不设设置 REUSEADDR 那么不能绑定成功.
通配地址
当不显示指明 ip 地址的时候, 一般需要下面的宏, 和变量
- //ipv4
- seraddr.sin_addr.s_addr=htonl(INADDR_ANY);
- //ipv6
- #include <netinet/in.h>
- ?
- extern in6addr_any;
- seraddr.sin6_addr=in6addr_any;
这里使用 htonl()原因在, 在套接字地址结构中的数据都是网络字节序的
bind()通常的错误是 EADDRINUSE 表示本端的这个套接字已经在使用了, 一般是地址. 端口不能重用
4.1.4 listen()
上面 bind 了套接字地址结构以后, 还没有开始监听啊
- #include <sys/socket.h>
- ?
- int listen(int sfd, int backlog);
- // 成功 0, 错误 - 1
listen()的主要作用:
将主动套接字转化为被动套接字
套接字的状态从 closed 到 listen
规定套接字排队的最大连接个数
已完成连接队列
当次队列为空的时候, accept()休眠
未完成连接队列
正在进行三次握手的连接
其实这里还有另一层意思, 当我们给以套接字描述符绑定本端的时候, 意味着我们可以读取这个文件描述符当对端发来数据以后, 我们从 listen 的文件描述符中读取已经连接的文件描述符监听套接字是一个只读套接字
4.1.5 accept()
该函数的作用是返回已完成连接队列的第一个连接的套接字描述符, 如果队列为空, 那么该函数将被阻塞(如果是阻塞套接字的话)
- #include <sys/socket.h>
- int accpet(int sfd, struct sockaddr *cliaddr, socklen_t *addrlen);
- // 成功描述符, 错误 - 1
cliaddr 是将要被内核填充的对端的套接字地址结构 addrlen 也是内核要填充的地址结构的长度
这两个参数都可以为 NULL
如果函数成功返回, 那么返回的是一个套接字描述符, 这个描述符关联了一对套接字地址结构, 因此可以当做普通文件描述符来使用
accpet()涉及到两个描述符, 一个是 sfd 称为监听描述符, 而函数返回的描述符是已连接套接字描述符, 我们使用这个文件描述符与对端进行数据交换
- 4.1.6 close()
- #include <unistd.h>
- int close(int fd);
- // 成功 0, 错误 - 1
这个函数的功能是, 将文件描述符的引用计数 - 1, 当为 0 的时候, 则关闭文件描述符, tcp 情况下触发四次挥手操作, 或是 rst
在 accept 返回的文件描述符应该及时关闭
4.1.7 get××name()
获取一个套接字描述符绑定一对套接字地址结构
- #include <sys/socket.h>
- int getsockname(int sfd, struct sockaddr *localaddr, socklen_t *addrlen);
- int getpeername(int sfd, struct sockaddr *peeraddr, socklen_t *addrlen);
- // 成功 0, 错误 - 1
这两个函数是用来让返回一个套接字描述符绑定的两个套接字地址结构
我们可以使用这两个函数:
getsockname 获取内核为我们选去的 ip 地址和端口号协议族
getpeername 可以返回对端的套接字地址结构(和 accpet 填充的一样?)
getpeername 是唯一能够在 accept 以后又调用 exec 函数后获得对端套接字地址结构的函数
返回的已完成连接套接字文件描述符不是 O_EXECLOSE, 因此在 exec 以后仍然打开, 同时, exec 以后, 所有的地址信息不能用了, 因此 accept 的就不能用了所以需要 getpeername 返回对端套接字地址结构
然后, 通过在 exec 的时候传入文件描述符, 或是在 exec 之前将文件描述符更改为 exec 要执行程序默认的一个文件描述符传递参数(inetd 采用第二种)
4.1.8 shutdown()
close 终止两个方向上的数据传输
- #include <sys/socket.h>
- int shutdown(int sockfd, int howto);
- // 成功 0, 错误 - 1
howto 表示行为:
SHUT_RD 关闭连接的读, 也就是套接字本端不再接受数据, 缓冲区现有数据被丢弃
也不能再使用读函数对套接字进行操作, 对 TCP 套接字该调用之后接受到的任何数据将被确认然后无声的丢弃掉
会超时然后 EPOLLERR? 应为不会发送 FIN.
SHUT_WR 关闭连接的写, 也就是本端不再写数据, 缓冲区中现有数据将被发送, 然后发送 FIN 分节
EPOLLIN 事件
SHUT_RDWR 第一次调用 SHUT_RD, 然后再调用 SHUT_WR
sockfd 文件描述符
使用 close 中止一个连接, 但它只是减少描述符的引用计数, 并不直接关闭连接, 只有当描述符的参考数为 0 时才关闭连接
shutdown 可直接关闭描述符, 不考虑描述符的参考数, 可选择中止一个方向的连接
来源: http://www.bubuko.com/infodetail-2543598.html