OC网络学习06:Socket实现

张建 lol

前言

Socket 起源于 Unix,而Unix基本哲学之一就是 一切皆文件,都可以用 打开open –> 读写write/read –> 关闭close 模式来操作。Socket 就是该模式的一个实现,网络的 Socket 数据传输是一种特殊的 I/OSocket 也是一种 文件描述符

什么是Socket?

Socket 俗称 套接字,是网络通信的基石。它可以通过 IP地址、端口号、TCP/UDP协议 实现客户端和服务端的双向通信。

Socket 属性

套接字的特性由3个属性确定,它们分别是:域、类型和协议

  • 套接字的 :它指定套接字通信中使用的网络介质,最常见的套接字域是 AF_INET,它指的是Internet网络。当客户使用套接字进行跨网络的连接时,它就需要用到服务器计算机的 IP地址和端口 来指定一台联网机器上的某个特定服务,所以在使用socket作为通信的终点,服务器应用程序必须在开始通信之前绑定一个端口,服务器在指定的端口等待客户的连接。另一个域 AF_UNIX 表示 UNIX 文件系统,它就是 文件输入/输出,而它的 地址就是文件名

  • 套接字 类型:因特网提供了两种通信机制:流(stream)和数据报(datagram),因而套接字的类型也就分为 流套接字数据报套接字

  • 流套接字 由类型 SOCK_STREAM 指定,它们是在 AF_INET 域中通过 TCP/IP 连接实现,同时也是 AF_UNIX 中常用的套接字类型。流套接字提供的是一个 有序、可靠、双向字节流 的连接,因此发送的数据可以 确保不会丢失、重复或乱序 到达,而且它还有一定的 出错后重新发送 的机制。

  • 与流套接字相对的是由类型 SOCK_DGRAM 指定的 数据报套接字,它 不需要建立连接和维持一个连接,它们在 AF_INET 中通常是通过 UDP/IP 协议实现的。它对可以发送的数据的长度有限制,数据报作为一个单独的网络消息被传输,它 可能会丢失、复制或错乱到达,UDP不是一个可靠的协议,但是它的速度比较高,因为它并非需要总是要建立和维持一个连接。

  • 套接字 协议:只要底层的传输机制允许不止一个协议来提供要求的套接字类型,我们就可以为套接字选择一个特定的协议。通常只需要使用默认值。

Socket 接口函数

既然 socketopen—>write/read—>close 模式的一种实现,那么 socket 就提供了这些操作对应的函数接口。下面以 TCP 为例,介绍几个基本的socket接口函数。

socket函数:使用给定的 协议族、套接字类型、协议编号(默认为0) 来创建套接字。

socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而 socket() 用于创建一个 socket描述符(socket descriptor),它唯一标识一个 socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

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

socket函数的三个参数分别为:

  • domain:协议域。常用的协议族有 AF_INET、AF_INET6 等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如 AF_INET 决定了要用 ipv4地址(32位的)与端口号(16位的) 的组合。

  • type:socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET 等等。

  • protocol:指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP 等,它们分别对应 TCP传输协议、UDP传输协议

注意:typeprotocol 不可以随意组合的,如 SOCK_STREAM 不可以跟 IPPROTO_UDP 组合。当 protocol0 时,会自动选择type类型对应的默认协议。

我们调用socket创建一个socket后,返回的socket描述符存在于协议族空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用 bind() 函数,否则就当调用 connect()、listen() 时系统会自动随机分配一个端口。

Socket原理

  1. Socket原理

1.1 概念

套接字(Socket)是通信的基石,是支持 TCP/IP 或者 UDP/IP 协议的网络通信的基本操作单元/编程接口(如下图)。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口

1.2、给套接字赋予地址

  • 依照建立套接字的目的不同,赋予套接字地址的方式有两种:服务端 使用 bind客户端 使用 connect

  • bind : 给服务器端中的套接字赋予通信的 地址和端口,IP和Port便可以区分一个 TCP/IP 链接通道,如果要区分特定的主机间链接,还需要提供Hostname。

  • connect : 客户端向特定网络地址的服务器发送连接请求。

1.3、建立Socket连接

  • 建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为 ClientSocket,另一个运行于服务器端,称为 ServerSocket

  • 套接字之间的连接过程分为三个步骤:服务器监听(bind、listen),客户端请求(connect),连接确认(accept)。

1.4 TCP连接

创建Socket链接时,可以制定不同的传输层协议(TCP或UDP),当使用TCP协议进行链接时,该Socket链接便是TCP链接。

  • TCP连接建立(三次握手)—-客户端执行connect触发

(1)第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,
Client进入 SYN_SENT 状态,等待Server确认。

(2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入 SYN_RCVD 状态。

(3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入 ESTABLISHED 状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

  • TCP连接终止(四次挥手)—- 客户端或服务端执行close触发

(1)第一次挥手:Client发送一个 FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1 状态。
(2)第二次挥手:Server收到 FIN 后,发送一个 ACK 给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入 CLOSE_WAIT 状态。
(3)第三次挥手:Server发送一个 FIN,用来关闭Server到Client的数据传送,Server进入 LAST_ACK 状态。
(4)第四次挥手:Client收到 FIN 后,Client进入 TIME_WAIT 状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入 CLOSED 状态,完成四次挥手。

2、客户端/服务器端模式的理解

首先服务器先启动对端口的监听,等待客户端的链接请求。

服务器端:

(1)服务器调用 socket 创建 Socket
(2) 服务器调用 bind 与特定 主机地址 与 端口号 绑定
(3)服务器调用 listen 让服务器监听客户端的请求;
(4)服务器通过 accept 接受客户端请求建立连接;
(5)服务器与客户端建立连接之后,就可以通过 send/recv 向客户端发送或从客户端接收数据;
(6)服务器调用 close 关闭 Socket

客户端:

(1)客户端调用 socket 创建 Socket;
(2)客户端调用 connect 向服务器发起连接请求以建立连接;
(3)客户端与服务器建立连接之后,就可以通过 send/recv 向客户端发送或从客户端接收数据;
(4)客户端调用 close 关闭 Socket;

接口介绍

  • 通用接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// socket 创建并初始化 socket,返回该 socket 的 文件描述符,如果描述符为 -1 表示创建失败。
int socket(int addressFamily, int type,int protocol)

// 关闭socket连接
int close(int socketFileDescriptor)

// 将 socket 与特定 主机地址与端口号 绑定,成功绑定返回 0,失败返回 -1。
int bind(int socketFileDescriptor,sockaddr *addressToBind,int addressStructLength)

// 接受客户端连接请求并将客户端的网络地址信息保存到 clientAddress 中。
int accept(int socketFileDescriptor,sockaddr *clientAddress, int clientAddressStructLength)

// 客户端向特定网络地址的服务器发送连接请求,连接成功返回 0,失败返回 -1。
int connect(int socketFileDescriptor,sockaddr *serverAddress, int serverAddressLength)

// 使用 DNS 查找特定主机名字对应的 IP 地址。如果找不到对应的 IP 地址则返回 NULL。
hostent* gethostbyname(char *hostname)

// 通过 socket 发送数据,发送成功返回成功发送的字节数,否则返回 -1。
int send(int socketFileDescriptor, char *buffer, int bufferLength, int flags)

// 从 socket 中读取数据,读取成功返回成功读取的字节数,否则返回 -1。
ssize_t recv(int, void *, size_t, int) __DARWIN_ALIAS_C(recv);

// 通过 UDP socket 发送数据到特定的网络地址,发送成功返回成功发送的字节数,否则返回 -1。
int sendto(int socketFileDescriptor,char *buffer, int bufferLength, int flags, sockaddr *destinationAddress, int destinationAddressLength)

// 从 UDP socket 中读取数据,并保存发送者的网络地址信息,读取成功返回成功读取的字节数,否则返回 -1 。
int recvfrom(int socketFileDescriptor,char *buffer, int bufferLength, int flags, sockaddr *fromAddress, int *fromAddressLength)

setsockopt 函数

  1. 设置套接字描述符的属性
1
int setsockopt(int, int, int, const void *, socklen_t);

参数:
sockfd:要设置的 套接字文件描述符
level:选项定义的层次。或为特定协议的代码(如IPv4,IPv6,TCP,SCTP),或为通用套接字代码(SOL_SOCKET)。
optname:选项名。level对应的选项,一个level对应多个选项,不同选项对应不同功能。
optval:指向某个变量的指针,该变量是要设置新值的缓冲区。可以是一个结构体,也可以是普通变量
optlen:optval的长度。

  • levelSOL_SOCKET 时,optname 可以有以下选项(一部分):
1
2
3
4
5
6
7
8
9
10
SO_BROADCAST      允许发送广播数据            int
SO_DEBUG        允许调试                int
SO_LINGER        延迟关闭连接          struct linger
SO_OOBINLINE      带外数据放入正常数据流         int
SO_RCVBUF        接收缓冲区大小             int
SO_SNDBUF        发送缓冲区大小             int
SO_RCVLOWAT      接收缓冲区下限             int
SO_SNDLOWAT      发送缓冲区下限             int
SO_RCVTIMEO       接收超时           struct timeval
SO_SNDTIMEO       发送超时           struct timeval
  • levelIPPROTO_IP 时,optname 可以有以下选项(一部分):
1
2
3
IP_HDRINCL       在数据包中包含IP首部          int
IP_OPTINOS       IP首部选项               int
IP_TTL          生存时间                int
  • levelIPPRO_TCP 时,optname 可以有以下选项(一部分)
1
2
TCP_MAXSEG       TCP最大数据段的大小          int
TCP_NODELAY       不使用Nagle算法            int
  • 返回值:

成功时返回0,失败时返回-1。

  1. send(),recv() 过程中有时 由于网络状况等原因,发收不能预期进行,而设置收发时限:
1
2
3
4
5
6
// 1000毫秒
int timeout = 1000;
// 发送时限
setsockopt(self.server_socket, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
// 接收时限
setsockopt(self.server_socket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
  1. send(),recv() 过程中,返回的是实际发送出去的字节(同步)或发送到socket缓冲区的字节(异步); 系统默认的状态发送和接收一次为8688字节(约为8.5K);在实际的过程中发送数据
    和接收数据量比较大,可以设置socket缓冲区,而避免了 send(),recv() 不断的循环收发:
1
2
3
4
5
6
// 缓冲区大小
int buf = 321024;
// 接收缓冲区
setsockopt(self.server_socket, SOL_SOCKET, SO_RCVBUF, &buf, sizeof(buf));
// 发送缓冲区
setsockopt(self.server_socket, SOL_SOCKET, SO_SNDBUF, &buf, sizeof(buf));
  1. send(),recv() 过程中,希望不经历由系统缓冲区到socket缓冲区的拷贝而影响程序的性能:
1
2
3
4
5
6
// 缓冲区大小
int buf = 0;
// 接收缓冲区
setsockopt(self.server_socket, SOL_SOCKET, SO_RCVBUF, &buf, sizeof(buf));
// 发送缓冲区
setsockopt(self.server_socket, SOL_SOCKET, SO_SNDBUF, &buf, sizeof(buf));
  1. 程序退出可以解除端口占用
1
2
3
// 程序退出后可以解除端口占用
int time = 1;
int result = setsockopt(self.server_socket, SOL_SOCKET, SO_REUSEADDR, &time, sizeof(time));
  1. 忽略 SIGPIPE 信号

SIGPIPE 信号会导致程序crash,其原因:

a.连接建立,若某一端关闭连接,而另一端仍然向它写数据,第一次写数据后会收到RST响应
b.此后再写数据,内核将向进程发出SIGPIPE信号,通知进程此连接已经断开。
c.而SIGPIPE信号的默认处理是终止程序,导致上述问题的发生。 为避免这种情况

处理方式一:直接忽略SIGPIPE信号

1
signal(SIGPIPE, SIG_IGN);

处理方式二:设置系统忽略SIGPIPIE消息

1
2
int optval = 1;
setsockopt(self.server_socket, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval));

shutdown() 函数

由于 socket双向 的,clientserver 都可以进行 读 和 写,因此,有时候我们需要数据在 socket 上实现单向传输,即数据 shutdown() 函数,单向的 socket 为 半开放socket,要实现半开放socket,需要用到 shutdown() 函数:

1
2
3
4
5
6
7
/* Shut down all or part of the connection open on socket FD.
HOW determines what to shut down:
SHUT_RD = No more receptions;
SHUT_WR = No more transmissions;
SHUT_RDWR = No more receptions or transmissions.
Returns 0 on success, -1 for errors. */
extern int shutdown (int __fd, int __how) __THROW;

a. SHUT_RD:断开输入流。套接字无法接收数据(即使缓冲区数据也会被清除),无法调用输入相关函数

b. SHUT_WD:断开输出流。套接字无法发送数据,但如果输出缓冲区中还有未传输的数据,则将传递到目标主机

c. SHUT_RDWR:同时断开 I/O 流,相当于分两次调用 shutdown(),其中一次以 SHUT_RD 为参数,另一次以 SHUT_WD 为参数

总结:shutdown() 用来关闭连接,而不是套接字

close() 函数

关闭文件描述符,即破坏当前进程所引用的 socket标识符,使其不再引用任何文件并且可以重用。

总结:close()用来关闭套接字

exit() 函数

EXIT_FAILUREEXIT_SUCESS 是C语言头文件库中定义的一个符号常量

1
2
3
#define EXIT_FIALURE 1

#difine EXIT_SUCCESS 0

exit(1)表示异常退出,在退出前给出一点提示信息,或在调查程序中看出错因

exit(0)表示正常退出

exit是系统调用级别,是一个函数,表示一个 进程的结束,exit实在调用处强制退出程序,运行一次程序就结束,这个状态标识了应用程序的一些运行信息,这个信息和机器和操作系统有关。

retun 函数

return关键字,表示了调用堆栈的返回,return用于结束一个函数的执行,将函数的执行信息传出供其他调用函数使用,如果返回的main函数,则为退出程序。

阻塞 和 非阻塞

  1. 定义
  • 阻塞:是指调用结果返回之前,调用者会进入阻塞状态等待。只有在得到结果之后才会返回。

  • 非阻塞:是指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

  1. 调用
  • 阻塞调用:比如 socketrecv(),调用这个函数的线程如果没有数据返回,它会一直阻塞着,也就是 recv() 后面的代码都不会执行了,程序就停在 recv() 这里等待,所以一般把 recv() 放在单独的线程里调用。

  • 非阻塞调用:比如非阻塞 socketsend(),调用这个函数,它只是把待发送的数据复制到TCP输出缓冲区中,就立刻返回了,线程并不会阻塞,数据有没有发出去 send() 是不知道的,不会等待它发出去才返回的。

服务器端函数

  1. bind函数:将套接字绑定到地址。
1
int bind(int sockfd, struct sockaddr * my_addr, int addrlen);

三个参数分别为:

  • sockfd : 即socket文件描述符,通过 socket() 函数创建,唯一标识一个socket。

  • my_addr : 结构体指针变量,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同。

  • addrlen : 对应的是地址的长度。

通常服务器在启动的时候都会绑定一个地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

  1. listen函数:使服务器的这个端口和IP处于监听状态,等待网络中某一客户机的连接请求。如果客户端有连接请求,端口就会接受这个连接。
1
int listen(int sockfd, int backlog);

两个参数分别为:

  • sockfd: socket文件描述符。

  • backlog: 指定 同时能处理的最大连接要求,通常为10或者5。最大值可设至128。参见 Socket_Listen 的详细说明。

  1. accept函数:接收监听套接字中保存的套接字连接,它提取监听套接字 sockfd 的挂起连接队列上的第一个连接请求,创建一个新的已连接套接字,并返回引用该套接字的新文件描述符。accept()执行的系统调用并不会对原始的监听套接字产生任何其他影响。接受远程计算机的连接请求,建立起与客户机之间的通信连接。服务器处于监听状态时,如果某时刻获得客户机的连接请求,此时并不是立即处理这个请求,而是将这个请求放在等待队列中,当系统空闲时再处理客户机的连接请求。
1
int accept(int sockfd, struct sockaddr * addr,int * addrlen);

三个参数分别为:

  • sockfd : socket文件描述符。

  • addr: 为结构体指针变量,和bind的结构体是同种类型的,系统会把远程主机的信息(远程主机的地址和端口号信息)保存到这个指针所指的结构体中。需要注意的是,该地址返回的是 客户端的外网IP,而客户端使用的IP一般均为内网IP,所以我们根据这个地址返回的IP是访问不到客户端主机的。

  • addrlen : 表示结构体的长度

accept的第一个参数为服务器的 socket文件描述符,是服务器开始调用 socket() 函数生成的,称为监听socket文件描述符;而accept函数返回的是已连接的socket文件描述符。一个服务器通常通常仅仅只创建一个监听socket文件描述符,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket文件描述符,当服务器完成了对某个客户的服务,相应的已连接socket文件描述符就被关闭。

客户端函数

connect函数 用来请求连接远程服务器.

1
int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);

三个参数分别为:

  • sockfd : socket文件描述符,前面socket的返回值;

  • serv_addr : 存储着远程服务器的IP与端口号信息;

  • addrlen : 表示结构体变量的长度。

通用函数

  1. recv函数:负责 从缓冲区中读取内容。当读成功时,read返回实际所读的字节数,如果返回的值是 0 表示已经读到文件的结束了,小于0 表示出现了错误。
1
int recv(int sockfd,void *buf,int len,unsigned int flags);

四个参数分别为:

  • sockfd : 为前面accept的返回值.也就是新的套接字。
  • buf : 表示缓冲区
  • len : 表示缓冲区的长度
  • flags : 通常为0
  1. send函数:将buf中的 n bytes 字节内容写入socket描述字。成功时返回写的字节数。失败时返回 -1,并设置 errno 变量。
1
int send(int sockfd,const void * msg,int len,unsigned int flags);
  • sockfd : 为前面socket的返回值.
  • msg : 一般为常量字符串
  • len : 表示长度
  • flags : 通常为0

flags参数可选值

  • MSG_NOSIGNAL:往读端关闭的管道或者socket连接中 写数据 时,不引发 SIGPIPE 信号
  • MSG_OOB:发送或接收紧急数据
  • MSG_PEEK:窥探读缓存中的数据,此次读操作不会导致这些数据被清除
  • MSG_WAITALL:读操作仅在读取到指定数量的字节后才返回
  • MSG_WAITSTREAM:读操作仅用在读取 TCP 协议的 SOCK_STREAM 指定数据才返回
  • MSG_MORE:告诉内核应用程序还有更多数据要发送,内核将超时等待新数据写入 TCP 发送缓冲区后一并发送。这样可防止 TCP 发送过多小的报文段,从而提高传输效率
  • MSG_DONTWAIT:对 socket 的此处操作是非阻塞的
  • MSG_DONTROUTE:不查看路由表,直接将数据发送给本地局域网内的主机。这表示发送者确切地知道目标主机救灾本地网络上
  • MSG_CONFIRM:指示数据链路层协议持续监听对应的回应,直到得到答复。它仅用于 SOCK_DGRAM 和 SOCK_RAW 类型的 socket
  1. close函数:关闭套接字。若顺利关闭则返回 0,发生错误时返回 -1
1
int close(int sockfd);

TCP 通信

TCP中 Socket 通信的基本步骤如下:

一个简单的 C/S 程序如下(客户端发出的数据, 服务器会回显到客户端的终端上。只是一个简单的模型, 没考虑错误处理等问题。)

编码原生客户端socket

  1. 导入头文件
1
2
3
4
5
6
7
8
#import <sys/socket.h>
#import <netinet/in.h>
#import <arpa/inet.h>

// htons : 将一个无符号短整型的主机数值转换为网络字节顺序,不同cpu 是不同的顺序 (big-endian大尾顺序 , little-endian小尾顺序)
#define SocketPort htons(8040)
// inet_addr是一个计算机函数,功能是将一个点分十进制的IP转换成一个长整数型数
#define SocketIP inet_addr("127.0.0.1")
  1. 创建socket
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
函数原型:
int socket(int domain, int type, int protocol);

domain:协议域,又称协议族(family)。常用的协议族有AF_INET(ipv4)、AF_INET6(ipv6)、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

type:指定Socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用。

protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
注意:type和protocol不可以随意组合,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当第三个参数为0时,会自动选择第二个参数类型对应的默认协议。

返回值:如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID_SOCKET(Linux下失败返回-1)。套接字描述符是一个整数类型的值。
*/
// 1.创建socket
// IPv4
// self.clientId = socket(AF_INET, SOCK_STREAM, 0);
// IPv6
self.clientId = socket(AF_INET6, SOCK_STREAM, 0);
if (self.clientId == -1) {
NSLog(@"创建socket失败");
return;
}else {
NSLog(@"创建socket成功");
}
  1. 连接socket
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 // 2.连接socket
// IPv4
// struct sockaddr_in socketAddr;
// socketAddr.sin_family = AF_INET;
// socketAddr.sin_port = socketPort;
//
// struct in_addr socketIn_addr;
// socketIn_addr.s_addr = socketIp;
//
// socketAddr.sin_addr = socketIn_addr;

// IPv6
struct sockaddr_in6 socketAddr;
socketAddr.sin6_family = AF_INET6;
socketAddr.sin6_port = socketPort;
socketAddr.sin6_addr = in6addr_loopback;

/*
函数原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:
sockfd:标识一个已连接套接口的描述字,就是我们刚刚创建的那个_clinenId。
addr:指针,指向目的套接字的地址。
addrlen:接收返回地址的缓冲区长度。
返回值:成功则返回0,失败返回非0,错误码GetLastError()。
*/
// 2. 连接socket
int result = connect(self.clientId, (const struct sockaddr *)&socketAddr, sizeof(socketAddr));
if (result != 0) {
NSLog(@"连接socket失败");
return;
}else {
NSLog(@"连接socket成功");
}
  1. 收到消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#pragma mark -接收消息
- (void)receiveMsg{
// 4. 接收数据
while (1) {
uint8_t buffer[1024];
ssize_t recvLen = recv(self.clientId, buffer, sizeof(buffer), 0);
NSLog(@"接收了%@字节",@(recvLen));

if (recvLen == 0) {
continue;
}

// buffer -> data -> string
NSData * data = [NSData dataWithBytes:buffer length:recvLen];
NSString * str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"接收到的字符串:%@",str);
}
}
  1. 发送消息
1
2
3
4
5
6
7
#pragma mark - 发送消息
- (void)sendMsg{
const char * msg = "50";
ssize_t sendLen = send(self.clientId, msg, strlen(msg), 0);
NSLog(@"发送了%@字节",@(sendLen));
NSLog(@"发送了字符串:%@",[NSString stringWithUTF8String:msg]);
}

编码原生服务端socket

相比客户端socket,服务端socket多了三步
bind(),listen(),accept()

  1. 头文件导入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import <sys/socket.h>
#import <netinet/in.h>
#import <arpa/inet.h>

/*
服务器可以使用终端命令:nc -lk SocketPort
也可以自己自己写一个本地socket服务端
*/
// htons : 将一个无符号短整型的主机数值转换为网络字节顺序,不同cpu 是不同的顺序 (big-endian大尾顺序 , little-endian小尾顺序)
#define SocketPort htons(8040)
// inet_addr是一个计算机函数,功能是将一个点分十进制的IP转换成一个长整数型数
#define SocketIP inet_addr("127.0.0.1")
// 最大连接次数
#define maxConnectCount 5
  1. 创建socket
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 创建socket

// Ipv4
self.serverID = socket(AF_INET, SOCK_STREAM, 0);
if (self.serverID == -1) {
NSLog(@"创建socket失败");
return;
} else {
NSLog(@"创建socket成功");
}

// IPv6
self.serverId = socket(AF_INET6, SOCK_STREAM, 0);
if (self.serverId == -1) {
NSLog(@"创建socket失败");
return;
}else {
NSLog(@"创建socket成功");
}
  1. 绑定socket
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 2.绑定socket到本地地址
// IPv4
// struct sockaddr_in socketAddr;
// socketAddr.sin_family = AF_INET;
// socketAddr.sin_port = socketPort;
//
// /* 服务端是等待别人来连,不需要找谁的ip
// 这里写一个长量 INADDR_ANY 表示 server 上所有ip,
// 这个一个server可能有多个ip地址,因为可能有多块网卡
// */
// struct in_addr socketIn_addr;
// socketIn_addr.s_addr = htonl(INADDR_ANY);
//
// socketAddr.sin_addr = socketIn_addr;

// IPv6
struct sockaddr_in6 socketAddr;
socketAddr.sin6_family = AF_INET6;
socketAddr.sin6_port = socketPort;
socketAddr.sin6_addr = in6addr_loopback;

if (bind(self.serverID, (const struct sockaddr *)&socketAddr, sizeof(socketAddr)) == -1) {
NSLog(@"绑定Socket失败");
return;
} else {
NSLog(@"绑定socket成功");
}
  1. 添加socket监听
1
2
3
4
5
6
7
8
// 3. 添加socket监听,让服务器监听客户端的请求
if (listen(self.serverID, maxConnectCount) == -1) {
NSLog(@"监听失败");
return;
} else {
NSLog(@"监听成功");
}

  1. 接收客户端请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// 4. accept , 当客户端发送请求时,程序为serverSocket创建一个新套接字 ConnectionSocket,用于clientSocket和serverSocket之间创建一个TCP连接
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self acceptSocket];
});

// 接收请求
- (void)acceptSocket{

while (1) {
// 4.接收客户端请求,当客户端发送请求时,程序为serverSocket创建一个新套接字
// IPv4
// struct sockaddr_in client_socketAddress;
// socklen_t address_len;
// // 打印客户端的请求地址
// NSLog(@"client socket is %d ip address is %s:%d\n",client_socketAddr,inet_ntoa(client_socketAddr),ntohs(client_address.sin_port));

// IPv6
struct sockaddr_in6 client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// 接受客户端连接 accept()能够返回的前提是正常完成三次握手
self.client_socket = accept(self.server_socket, (struct sockaddr *)&client_addr, &client_addr_len);
NSLog(@"client socket ip address is %@ port is %hu\n",[self hostFromSockaddr6:&client_addr],[self portFromSockaddr6:&client_addr]);
if (self.client_socket > 0) {
NSLog(@"%@\n 连接socks5成功,socketId is %d",self,self.client_socket);
}else if (self.client_socket == 0) {
NSLog(@"%@\n 连接socks5关闭",self);
close(self.client_socket);
}else {
NSLog(@"%@\n 连接socks5错误 error:%s",self,strerror(errno));
close(self.client_socket);
}

// 5. 接收VPN转发的数据
char resdata[1024] = {0};
long readLen = recv(self.client_socket, resdata, 1024, 0);
if (readLen > 0){
NSLog(@"%@\n 收到VPN转发的数据成功 %ld字节",self,readLen);
}else{
NSLog(@"%@\n 收到VPN转发的数据报错 %s",self,strerror(errno));
close(self.client_socket);
}

// 6.向VPN发送50
// 50
unsigned char welcome_message[] = {5,0};
/*
注意发送时的套接字是连接套接字,而不是服务器的套接字
发送成功返回发送成功的字节数
*/
long sendDataLen = send(self.client_socket, welcome_message, 2, 0);
if (sendDataLen > 0) {
NSLog(@"%@\n 发送了50成功 %ld字节",self,sendDataLen);
}else {
NSLog(@"%@\n 发送了50失败",self);
close(self.client_socket);
}

// 7.继续从VPN接收数据
// 用于存放接收数据的缓冲区
char buf[1024] = {0};
/*
读取数据,读取成功返回成功读取的字节数
*/
readLen = recv(self.client_socket, buf, sizeof(buf), 0);
if (readLen > 0) {
// 消息(为啥会调用3次,每次结果一样)
NSData * recvData = [[NSData alloc] initWithBytes:buf length:readLen];
NSString * recvStr = [[NSString alloc] initWithData:recvData encoding:NSASCIIStringEncoding];
NSLog(@"%@\n 收到VPN消息成功:%@ %ld字节",self,recvStr,readLen);

// 发送消息到网关服务器
[self sendMsgToGwServerWithData:recvData];
}else if (readLen == 0) {
NSLog(@"%@\n 客户端已关闭",self);
close(self.client_socket);
}else {
/* strerror(errno) 打印报错信息
报错:Connection reset by peer 是连接断开后的读和写操作引起的。
1、一端socket被关闭,另一端发送数据,发送的第一个数据包引发该异常(socket默认连接60秒,60秒内没有进行心跳交互,即读写数据,就会自动关闭连接)
2、一端退出,但退出未关闭该连接,另一端如果在从连接中读数据,则抛出该异常
*/
NSLog(@"%@\n 收到VPN消息失败 %s",self,strerror(errno));
close(self.client_socket);
}
}
}

UDP Socket函数

  • sendto() 函数:发送 UDP 数据,将数据发送到套接字。返回实际发送的数据字节长度或在出现发送错误时返回 -1
1
int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen);
  • recvfrom() 函数:接受 UDP 套接字的数据, 与 recv() 类似。返回接收到的字节数或当出现错误时返回 -1,并置相应的 errno
1
int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen);

UDP通信流程图如下:

长链接 和 短链接

  • 长链接:连上就一直聊,通常用于 及时通讯,效率高。(一对一:)

  • 短链接:通讯一次,马上断开,下次再建立连接,效率低。(一对多)

及时通讯:客户端和服务端是 短链接,客户端与客户端是 长链接

Socket如何保持长链接

一般的Socket正常收发完消息之后,就会断开连接(主动或被动),但是有些实时化场景要求高的地方,需要及时收发消息,比如 直播间、股票期货行情模块 等,要实时收发数据,这样的话就需要Socket保持连接一直在。

方法:发送 心跳包 来保活

  1. 应用层自己实现 心跳包
  • 服务器在 NSTimer 中定时向客户端发送一个小数据包,如果客户端一定时间内没有回应,即认为客户端已掉线。

  • 同理:客户端在 NSTimer 中定时向 服务端 发送一个小数据包,在一定时间内没有收到服务器的心跳包,则认为连接不可用。

  1. TCPKeepAlive 保活机制

考虑到一个服务器通常会连接多个客户端,因此由用户在应用层自己实现心跳包,代码较多且稍显复杂,而利用 TCP/IP协议层 为内置的 KeepAlive 功能来实现心跳功能则简单得多。 不论是服务端还是客户端,一方开启 KeepAlive 功能后,就会自动在规定时间内向对方发送心跳包, 而另一方在收到心跳包后就会自动回复,以告诉对方我仍然在线。

粘包、分包(拆包)

  1. 概念

Socket通信时会对发送的字节数据进行 分包粘包 处理,属于一种Socket内部的优化机制。

  1. 粘包:

当发送的 字节数据包比较小且频繁 发送时,Socket内部会将字节数据进行 粘包处理,既将频繁发送的小字节数据打包成 一个整包进行发送,降低内存的消耗。

  1. 分包:

当发送的 字节数据包比较大 时,Socket内部会将发送的字节数据进行 分包处理,降低内存和性能的消耗。

  1. 例子解释
1
2
3
当前发送方发送了两个包,两个包的内容如下:
123456789
ABCDEFGH

我们希望接收方的情况是:收到两个包,第一个包为:123456789,第二个包为:ABCDEFGH。
但是在粘包和分包出现的情况就达不到预期情况。

  • 粘包情况:

两个包在很短的时间间隔内发送,比如在0.1秒内发送了这两个包,如果包长度足够的话,那么接收方只会接收到一个包,如下:

1
123456789ABCDEFGH
  • 分包情况

假设包的长度最长设置为5字节(较极端的假设,一般长度设置为1000到1500之间),那么在没有粘包的情况下,接收方就会收到4个包,如下:

1
2
3
4
12345
6789
ABCDE
FGH
  1. 处理方式

因为存在粘包和分包的情况,所以接收方需要对接收的数据进行一定的处理,主要解决的问题有两个:

  • 在粘包产生时,要可以在同一个包内获取出多个包的内容。
  • 在分包产生时,要保留上一个包的部分内容,与下一个包的部分内容组合。

处理方式:

  • 在数据包头部加上内容长度以及数据类型

发送数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#pragma mark - 发送数据格式化
- (void)sendData:(NSData *)data dataType:(unsigned int)dataType{
NSMutableData *mData = [NSMutableData data];
// 1.计算数据总长度 data
unsigned int dataLength = 4+4+(int)data.length;
// 将长度转成data
NSData *lengthData = [NSData dataWithBytes:&dataLength length:4];
// mData 拼接长度data
[mData appendData:lengthData];

// 数据类型 data
// 2.拼接指令类型(4~7:指令)
NSData *typeData = [NSData dataWithBytes:&dataType length:4];
// mData 拼接数据类型data
[mData appendData:typeData];

// 3.最后拼接真正的数据data
[mData appendData:data];
NSLog(@"发送数据的总字节大小:%ld",mData.length);

// 发数据
[self.socket writeData:mData withTimeout:-1 tag:10086];
}

接收数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)recvData:(NSData *)data{
// 直接就给他缓存起来
[self.cacheData appendData:data];
// 获取总的数据包大小
// 整段数据长度(不包含长度跟类型)
NSData *totalSizeData = [data subdataWithRange:NSMakeRange(0, 4)];
unsigned int totalSize = 0;
[totalSizeData getBytes:&totalSize length:4];
//包含长度跟类型的数据长度
unsigned int completeSize = totalSize + 8;
//必须要大于8 才会进这个循环
while (self.cacheData.length>8) {
if (self.cacheData.length < completeSize) {
//如果缓存的长度 还不如 我们传过来的数据长度,就让socket继续接收数据
[self.socket readDataWithTimeout:-1 tag:10086];
break;
}
//取出数据
NSData *resultData = [self.cacheData subdataWithRange:NSMakeRange(8, completeSize)];
//处理数据
[self handleRecvData:resultData];
//清空刚刚缓存的data
[self.cacheData replaceBytesInRange:NSMakeRange(0, completeSize) withBytes:nil length:0];
//如果缓存的数据长度还是大于8,再执行一次方法
if (self.cacheData.length > 8) {
[self recvData:nil];
}
}
}

iOS端实现 Socket

GCDAsyncSocket 框架

  1. 创建GCDAsyncSocket对象,并且使之成为成员属性
1
GCDAsyncSocket *_socket;
  1. 我们在这里传入一个全局队列,让它工作在子线程,防止网络不畅时阻塞主线程。
1
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
  1. 连接服务端
1
2
3
4
5
6
7
8
9
// IP地址
NSString *host = @"127.0.0.1";
// 端口号
int port = 12345;
NSError *error = nil;
[_socket connectToHost:host onPort:port error:&error];
if (error) {
NSLog(@"%@",error);
}
  1. 实现代理方法来获取数据
1
2
3
4
5
6
7
8
9
10
11
// 连接成功的代理
-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
NSLog(@"成功连接到%@:%d",host,port);
}

// 连接结束的代理
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
if (err) {
NSLog(@"%@",err);
}
}
  • 接收数据的代理

这里的 tag 是用于 区分不同的消息 的,在写一条消息的时候需要指定tag,通过不同的tag判断服务器返回的消息的类型。这里有两类消息,分别是 登录消息和聊天消息,只有后者会被显示,reloadDataWithText 是用于 tableView 显示数据的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 数据成功发送到服务器
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
// 需要自己调用读取方法,socket才会调用代理方法读取数据
[_socket readDataWithTimeout:-1 tag:tag];

}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{

switch (tag) {
case LoginTag:
break;
case MsgTag:{
NSString *msg = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[self reloadDataWithText:msg];
break;
}
default:
break;
}

}
  1. 发送一条消息
  • 使用socket的 writeData 方法,这里不需要指定消息的长度和缓冲区大小,十分方便,tag会被传入,在调用上面提到的代理方法 (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag 时会被传入,用于判断消息类型。
1
2
3
4
5
// 发送 iam:name 表示name登录
NSString *loginStr = @"iam:soulghost";
// 把string转成NSData
NSData *data = [loginStr dataUsingEncoding:NSUTF8StringEncoding];
[_socket writeData:data withTimeout:-1 tag:LoginTag];
  • 需要注意的是异步socket工作在子线程,如果要更新UI,必然会在socket的代理方法中调用更新UI的方法,这时更新UI的代码运行于子线程,不能立即刷新UI界面,因此应该把更新UI的函数放在主线程中执行:
1
2
3
dispatch_async(dispatch_get_main_queue(), ^{
// 更新UI的代码
});

错误码 errno

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Try again */
#define ENOMEM 12 /* Out of memory */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Device or resource busy */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Not a typewriter */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */
#define EROFS 30 /* Read-only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Math argument out of domain of func */
#define ERANGE 34 /* Math result not representable */
#define EDEADLK 35 /* Resource deadlock would occur */
#define ENAMETOOLONG 36 /* File name too long */
#define ENOLCK 37 /* No record locks available */
#define ENOSYS 38 /* Function not implemented */
#define ENOTEMPTY 39 /* Directory not empty */
#define ELOOP 40 /* Too many symbolic links encountered */
#define EWOULDBLOCK EAGAIN /* Operation would block */
#define ENOMSG 42 /* No message of desired type */
#define EIDRM 43 /* Identifier removed */
#define ECHRNG 44 /* Channel number out of range */
#define EL2NSYNC 45 /* Level 2 not synchronized */
#define EL3HLT 46 /* Level 3 halted */
#define EL3RST 47 /* Level 3 reset */
#define ELNRNG 48 /* Link number out of range */
#define EUNATCH 49 /* Protocol driver not attached */
#define ENOCSI 50 /* No CSI structure available */
#define EL2HLT 51 /* Level 2 halted */
#define EBADE 52 /* Invalid exchange */
#define EBADR 53 /* Invalid request descriptor */
#define EXFULL 54 /* Exchange full */
#define ENOANO 55 /* No anode */
#define EBADRQC 56 /* Invalid request code */
#define EBADSLT 57 /* Invalid slot */
#define EDEADLOCK EDEADLK
#define EBFONT 59 /* Bad font file format */
#define ENOSTR 60 /* Device not a stream */
#define ENODATA 61 /* No data available */
#define ETIME 62 /* Timer expired */
#define ENOSR 63 /* Out of streams resources */
#define ENONET 64 /* Machine is not on the network */
#define ENOPKG 65 /* Package not installed */
#define EREMOTE 66 /* Object is remote */
#define ENOLINK 67 /* Link has been severed */
#define EADV 68 /* Advertise error */
#define ESRMNT 69 /* Srmount error */
#define ECOMM 70 /* Communication error on send */
#define EPROTO 71 /* Protocol error */
#define EMULTIHOP 72 /* Multihop attempted */
#define EDOTDOT 73 /* RFS specific error */
#define EBADMSG 74 /* Not a data message */
#define EOVERFLOW 75 /* Value too large for defined data type */
#define ENOTUNIQ 76 /* Name not unique on network */
#define EBADFD 77 /* File descriptor in bad state */
#define EREMCHG 78 /* Remote address changed */
#define ELIBACC 79 /* Can not access a needed shared library */
#define ELIBBAD 80 /* Accessing a corrupted shared library */
#define ELIBSCN 81 /* .lib section in a.out corrupted */
#define ELIBMAX 82 /* Attempting to link in too many shared libraries */
#define ELIBEXEC 83 /* Cannot exec a shared library directly */
#define EILSEQ 84 /* Illegal byte sequence */
#define ERESTART 85 /* Interrupted system call should be restarted */
#define ESTRPIPE 86 /* Streams pipe error */
#define EUSERS 87 /* Too many users */
#define ENOTSOCK 88 /* Socket operation on non-socket */
#define EDESTADDRREQ 89 /* Destination address required */
#define EMSGSIZE 90 /* Message too long */
#define EPROTOTYPE 91 /* Protocol wrong type for socket */
#define ENOPROTOOPT 92 /* Protocol not available */
#define EPROTONOSUPPORT 93 /* Protocol not supported */
#define ESOCKTNOSUPPORT 94 /* Socket type not supported */
#define EOPNOTSUPP 95 /* Operation not supported on transport endpoint */
#define EPFNOSUPPORT 96 /* Protocol family not supported */
#define EAFNOSUPPORT 97 /* Address family not supported by protocol */
#define EADDRINUSE 98 /* Address already in use */
#define EADDRNOTAVAIL 99 /* Cannot assign requested address */
#define ENETDOWN 100 /* Network is down */
#define ENETUNREACH 101 /* Network is unreachable */
#define ENETRESET 102 /* Network dropped connection because of reset */
#define ECONNABORTED 103 /* Software caused connection abort */
#define ECONNRESET 104 /* Connection reset by peer */
#define ENOBUFS 105 /* No buffer space available */
#define EISCONN 106 /* Transport endpoint is already connected */
#define ENOTCONN 107 /* Transport endpoint is not connected */
#define ESHUTDOWN 108 /* Cannot send after transport endpoint shutdown */
#define ETOOMANYREFS 109 /* Too many references: cannot splice */
#define ETIMEDOUT 110 /* Connection timed out */
#define ECONNREFUSED 111 /* Connection refused */
#define EHOSTDOWN 112 /* Host is down */
#define EHOSTUNREACH 113 /* No route to host */
#define EALREADY 114 /* Operation already in progress */
#define EINPROGRESS 115 /* Operation now in progress */
#define ESTALE 116 /* Stale NFS file handle */
#define EUCLEAN 117 /* Structure needs cleaning */
#define ENOTNAM 118 /* Not a XENIX named type file */
#define ENAVAIL 119 /* No XENIX semaphores available */
#define EISNAM 120 /* Is a named type file */
#define EREMOTEIO 121 /* Remote I/O error */
#define EDQUOT 122 /* Quota exceeded */
#define ENOMEDIUM 123 /* Nomedium found */
#define EMEDIUMTYEP 124 /*Wrongmedium found */
#define ECANCELED 125 /* Operation Canceled */
#define ENOKEY 126 /* Required key not available */
#define EKEYEXPIRED 127 /* Key has expired */
#define EKEYREVOKED 128 /* Key has been revoked */
#define EKEYREJECTED 129 /* Key was rejected by service */
#define EOWNERDEAD 130 /* Owner died */
#define ENOTRECOVERABLE 131 /* State not recoverable */
#define ERFKILL 132 /* Operation not possible due to RF-kill */
#define EHWPOISON 133 /* Memory page has hardware error */

Broken pipe

  • Broken pipe 的字面意思是 管道破裂Broken pipe 的原因是该管道的读端被关闭。

  • Broken pipe 经常发生socket关闭之后(或者其他的描述符关闭之后)的write操作中

  • 发生 Broken pipe 错误时,进程收到 SIGPIPE 信号,默认动作是进程终止。

  • Broken pipe 最直接的意思是:写入端出现的时候,另一端却休息或退出了,因此造成没有及时取走管道中的数据,从而系统异常退出;

  • Post title:OC网络学习06:Socket实现
  • Post author:张建
  • Create time:2023-04-22 20:20:45
  • Post link:https://redefine.ohevan.com/2023/04/22/OC网络/OC网络学习06:Socket实现/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.