伯克利套接字(Berkeley sockets),也称为BSD Socket。伯克利套接字的应用编程接口(API)是采用C语言的进程间通信的库,经常用在计算机网络间的通信。 BSD Socket的应用编程接口已经是网络套接字的抽象标准。大多数其他程序语言使用一种相似的编程接口。它最初是由加州伯克利大学为Unix系统开发出来的。所有现代的操作系统都实现了伯克利套接字接口,因为它已经是连接互联网的标准接口了。
API函数
以下函数是最基本的 socket API
-
socket()
创造某种类型的套接字,分配一些系统资源,用返回的整数识别。 -
bind()
一般是用在服务器这边,和一个套接字地址结构相连,比如说是一个特定的本地端口号和一个IP地址。 -
listen()
用在服务器一边,导致一个绑定的TCP套接字进入监听状态。 -
connect()
用在客户机这边,给套接字分配一个空闲的端口号。比如说一个TCP套接字,它会试图建立一个新的TCP连接。 -
accept()
用在服务器这边。从客户机那接受请求试图创造一个新的TCP连接,并把一个套接字和这个连接相联系起来。 -
send()
andrecv()
, orwrite()
andread()
, orsendto()
andrecvfrom()
用来接收和发送数据。 -
close()
当套接字的引用计数为0的时候才会引发TCP的四次挥手,关闭连接,系统释放资源。------- 不可对某个socket连续调用两次close,否则第二次调用会出现释放未分配的内存问题(野指针)(在LWIP下测试得出的结论)。个人想法:应该在close函数里面把socket置成某个数,这样每次进入close,如果socket等于某个数,表示已经close过,直接函数返回。 - shutdown() 不用管套接字的引用计数,调用读写函数返回小于0的数,退出阻塞。
以下函数用来设置 / 获取套接字的属性,有些函数的功能有重叠;fcntl() 和 ioctl() 功能比较强大,在linux c 中不仅仅用来网络编程
-
gethostbyname()
andgethostbyaddr()
用来解析主机名和地址。 -
select()
------ 用来获取指定套接字的状态(可读、可写或者出错) - fcntl() ------ 设置套接字的工作模式(阻塞或非阻塞)
- ioctl()
-
poll()
is used to check on the state of a socket in a set of sockets. The set can be tested to see if any socket can be written to, read from or if an error occurred. -
getsockopt()
------ 获得套接字的属性,有些属性的开关在 opt.h 和 lwipopts.h -
setsockopt()
------ 设置套接字的属性,有些属性的开关在 opt.h 和 lwipopts.h - getsockname()、getpeername() ------ 用于获取本地、对方IP和端口号
以下函数用来数据的转换
- ntohs()、ntohl()、htons()、htonl() ------ 大端编码和小端编码的转换,s表示short(16bit),l表示long(32bit);h 代表 host,就是本地主机的表示形式; n 代表 network,表示网络上传输的字节序
- inet_ntoa() 、inet_aton()、inet_addr() ----- inet_ntoa()将一个 32bits 无符号整数转换为点分十进制IP格式的字符串,inet_addr()正好相反
- inet_pton()、inet_ntop() ---- inet_pton() 的功能和 inet_addr() 功能类似
- IP4_ADDR --- 把 IP地址中的4个数字整合放到 u32 类型的变量
更多的函数说明如下:
recv、send和read、write
int recv(int sockfd,void *buf,int len,int flags)
int send(int sockfd,void *buf,int len,int flags)
对于send(),flags取值有:
0: 与write()无异,阻塞操作
MSG_DONTROUTE:告诉内核,目标主机在本地网络,不用查路由表
MSG_DONTWAIT:将单个I/O操作设置为非阻塞模式
MSG_OOB:指明发送的是带外信息 对于recv(),flags取值有:
0:与read()无异,阻塞操作
MSG_DONTWAIT:将单个I/O操作设置为非阻塞模式
MSG_OOB:指明发送的是带外信息
MSG_PEEK:表示只是从系统缓冲区中读取内容,而不清除系统缓冲区的内容.这样下次读的时候,仍然是一样的内容.一般在有多个进程读写数据时可以使用这个标志.
MSG_WAITALL:通知内核直到读到请求的数据字节数时,或者发送了错误才返回。 对于接收数据函数的返回值:
大于0:读到数据的字节数
等于0:套接字关闭,读到FIN
小于0:
EINTR:表示操作被中断,下次可以继续读取,忽视这个返回值
EWOULDBLOCK(EAGAIN):当非阻塞读取时,没有数据可读,返回此操作码
ECONNRESET:对方发送RST
...... 对于发送数据函数的返回值:
大于0:成功发送数据的字节数
等于0:套接字关闭
小于0:
EINTR:表示连接正常,操作被中断,此次数据发送失败,下次可以继续发送
EWOULDBLOCK(EAGAIN):表示连接正常,但发送缓冲区没有空间,此次数据发送失败,下次可以继续发送
...... 注:不能通过 shutdown 读写通道主动退出服务器的阻塞read(),但是可以主动退出退出客户端的阻塞read(),ESP8266测试得到
recvfrom、sendto
这两个函数用在UDP通信的接收和发送
int recefrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen)
int sendto(int sockfd, const void *buf, size_t size, int flags, const struct sockaddr *to, socklen_t tolen)
socket()
函数原型:
int socket(int domain, int type, int protocol);
socket()
为通信创造一个端点并返回一个文件描述符。 socket()
有三个参数:
-
domain,确定地址协议。例如:
-
PF_INET
(AF_INET)是IPv4 -
PF_INET6
是 IPv6.
-
-
type,是下面中的一个:
-
SOCK_STREAM
(Stream Socket) -
SOCK_DGRAM
(Datagram Socket) -
SOCK_RAW
(Raw Socket)。
-
-
protocol, 规定套接字发送和接送哪类型协议数据。最常见的是
IPPROTO_TCP
,IPPROTO_UDP
,IPPROTO_UDPLITE、IPPROTO_ICMP
。如果domain
和type
已经确定唯一的协议,“0(
IPPROTO_IP
)
” 可以用来表示选择一个默认的协议。
例子:
tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_IP); // 由于 PF_INET 和 SOCK_STREAM 已经可以确认是使用 IPPROTO_TCP,所以第三个参数填什么都不影响 udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_IP); // PF_INET 和 SOCK_DGRAM 表明使用 IPPROTO_UDP 或 IPPROTO_UDPLITE 其中一种协议,第三个参数填的不是 IPPROTO_UDPLITE 都是IPPROTO_UDP
如果出错返回-1,否则返回一个代表文件描述符的整数(一般有个宏NUM_SOCKETS规定一共可以创建多少个socket,所以socket()创建成功返回的整数范围为0~NUM_SOCKETS-1,而且是从小到大返回)
bind()
bind()
给套接字分配一个地址。当使用 socket()
创造一个套接字时, 只是给定了协议族,并没有分配地址。在套接字能够接受来自其他主机的连接前,必须用bind()给它绑定一个地址。 bind()
由三个参数:
-
sockfd
, 代表socket的文件描述符。 -
my_addr
, 指向sockaddr
结构体的指针,代表要绑定的地址 。 -
addrlen
, 是sockaddr结构体的大小。
Bind()返回0表示成功,错误返回-1。
- 函数原型:
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
listen()
一旦一个套接字和一个地址联系之后,listen()
监听到来的连接。但是这只适用于对面向连接的模式,例如 套接字类型是 (SOCK_STREAM
, SOCK_SEQPACKET
)。listen()需要两个参数:
-
sockfd:
一个有效的套接字描述符。 backlog:accept 队列大小。当服务器接收到第三次握手后将连接放入这个队列中,直到被 accept 处理才清除,当 accept 队列满了之后,即使 client 继续向 server 发送 ACK 包,也不会被响应,此时 ListenOverflows+1,同时 server 通过 /proc/sys/net/ipv4/tcp_abort_on_overflow(linux kernel 2.2 之后)来决定如何返回,0表示直接丢弃该 ACK,1表示发送 RST 通知 client;相应的,client 则会分别返回 read timeout 或者 connection reset by peer。参考下图。
一旦连接被接受,返回0表示成功,错误返回-1。
- 函数原型:
int listen(int sockfd, int backlog);
accept()
当应用程序监听来自其他主机的面对数据流的连接时,通过事件(比如Unix select()系统调用)通知它。必须用accept()
函数初始化连接。 Accept() 为每个连接创立新的套接字并从监听队列中移除这个连接。它使用如下参数:
-
sockfd
,监听的套接字描述符 -
cliaddr
, 指向sockaddr 结构体的指针,客户机地址信息。 -
addrlen
,指向socklen_t
的指针,确定客户机地址结构体的大小 。
返回新的套接字描述符,出错返回-1。和客户端的通信是通过这个套接字。假如是 sockfd(服务器套接字,accept 第一个参数) 0,accpet 返回的是 1,递增。可以在没关闭 accept 返回的套接字之前关闭 accept 的套接字,这样防止新的客户端连上。但是有个问题,如果关闭服务器套接字,accept 返回一个套接字是1,用 select 函数的第一个参数填套接字总数,现在的总是是1,而唯一个套接字的值是1(不是0),所以 select 会返回小于0,不能正常使用 select 函数。
Datagram 套接字不要求用accept()处理,因为接收方可能用监听套接字立即处理这个请求。
- 函数原型:
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
注:不能通过 close() 或者 shutdown() 主动退出 accept() 阻塞,ESP8266测试得出。
connect()
connect()
系统调用为一个套接字设置连接,参数有文件描述符和主机地址。
- 函数原型:
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
close()
close()函数只是将这个套接字引用计数减1,就像rm一样,删除一个文件时只是移除一个硬链接数,只有这个套接字的所有引用计数减到0,套接字描述符才会真的被关闭,才会开始后续的四次挥手。
关于引用计数:操作系统创建子进程时,子进程将继承父进程打开的套接字,父子进程拥有对该套接字同样的访问权,系统对每个套接字的引用进行计数,每增加一个进程访问套接字,计数加1,当进程完成对套接字的使用时,对套接字的使用调用 close 进行关闭,close 调用将减少套接字的引用计数,并且在计数值为0时删除该套接字
SO_LINGER选项对close()的行为有影响
SO_LINGER 的 I_onoff 为0(默认情况):
在套接字上不能再发出发送或接收请求,套接字发送缓冲区中的内容被发送到对端,如果描述符引用计数变为0,在发送完发送缓冲区的数据后,后跟正常的TCP连接终止序列(即发送FIN),套接字接收缓冲区中的内容被丢弃
SO_LINGER 的 l_onoff = 1,l_linger = 0:
在套接字上不能再发出发送或接收请求,如果描述符引用计数变为0,RST被发送到对端,连接的状态被置为CLOSED(没有TIME_WAIT状态),套接字发送缓冲区和接收缓冲区中的内容被丢弃;
shutdown()
可以指定关闭读通道、写通道或者读写通道。
ESP8266 LWIP TCP客户端情况下(不推荐用shutdown退出阻塞,推荐用select()):
调用了shutdown()的读通道,不会引发四次挥手,但是执行read()会退出阻塞,返回小于0的数,然后再close()引发四次挥手,释放资源。
特别强调:执行 shutdown() 后执行 close(),然后再执行read()会阻塞,永远的阻塞。。。
gethostbyname() 和 gethostbyaddr()
gethostbyname()
和 gethostbyaddr()
函数是用来解析主机名和地址的。可能会使用DNS服务或者本地主机上的其他解析机制(例如查询/etc/hosts)。返回一个指向 struct hostent的指针,这个结构体描述一个IP主机。函数使用如下参数:
- name 指定主机名。例如 www.wikipedia.org
- addr 指向 struct in_addr的指针,包含主机的地址。
- len 给出 addr的长度,以字节为单位。
- type 指定地址族类型 (比如 AF_INET)。
出错返回NULL指针,可以通过检查 h_errno 来确定是临时错误还是未知主机。正确则返回一个有效的 struct hostent *。
这些函数并不是伯克利套接字严格的组成部分。这些函数可能是过时了,新函数是 getaddrinfo() and getnameinfo(), 这些新函数是基于addrinfo数据结构。
- 函数原型:
struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const void *addr, int len, int type);
setsockopt()、getsockopt()
int setsockopt( int socket, int level, int option_name, const void *option_value, size_t option_len);
sockfd: 套接字
level: 协议层(SOL_SOCKET、IPPROTO_IP、IPPRO_TCP)
opt_name: 选项名,每一个协议层都有其一些固定的选项名,
option_value: 缓冲区,setsockopt() 是指向将要存放的地址,getsockopt() 是指向目前存放信息的地址
option_len: 缓冲区大小长度
在 socket.h 有 level 和 opt_name 的介绍
SOL_SOCKET层,有如下选项:
opt_name | 说明 | option_value类型 |
SO_BROADCAST | 允许发送和接收广播数据 | int |
SO_DEBUG | 允许调试 | int |
SO_DONTROUTE | 不查找路由 | int |
SO_ERROR | 获得套接字错误 | int |
SO_KEEPALIVE | 保持连接 | 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 |
SO_REUSERADDR | 允许重用本地地址和端口 | int |
SO_TYPE | 获得套接字类型 | int |
SO_BSDCOMPAT | 与BSD系统兼容 | int |
...... |
IPPROTO_TCP层,有如下选型:
TCP_NODELAY
TCP_KEEPIDLE(设置多久没接收到数据开始发送keepalive包)
TCP_KEEPINTVL(设置每个keepalive包的间隔时间)
TCP_KEEPCNT(设置发送多少个keepalive包)
......
ntohs()、ntohl()、htons()、htonl()
ntohs(n) //n为16位数据类型,网络字节顺序到主机字节顺序的转换
htons(n) //n为16位数据类型,主机字节顺序到网络字节顺序的转换
ntohl(n) //n为32位数据类型,网络字节顺序到主机字节顺序的转换
htonl(n) //n为32位数据类型,主机字节顺序到网络字节顺序的转换
网络字节顺序采用大端模式进行编址,而主机字节顺序根据处理器的不同而不同,如PowerPC处理器使用大端模式,而Pentuim处理器使用小端模式。大端模式处理器的字节序到网络字节序不需要转换,此时ntohs(n)=n,ntohl(n) = n;而小端模式处理器的字节序到网络字节必须要进行转换
select()
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout)
本函数用于确定一个或多个套接口的状态。对每一个套接口,调用者可查询它的可读性、可写性及错误状态信息。用结构体 fd_set 来表示一组等待检查的套接口。在调用返回时,这个结构存有满足一定条件的套接口组的子集,并且select()返回满足条件的套接口的数目,0表示超时,-1表示出错。有一组宏可用于对 fd_set 的操作(宏的功能有:把套接口放入 fd_set 结构、清除 fd_set 结构中的某个套接口、检查否个套接口是否可读可写等)
maxfdp1:等于创建成功的socket个数,socket()成功返回的整数范围为0~NUM_SOCKETS-1,NUM_SOCKETS为最多能创建的个数,所以maxfdp1一般取值为socket()返回的最大值+1
readset:(可选)指针,指向一组等待可读性检查的套接口。如果该套接口正处于监听listen()状态,则若有连接请求到达,该套接口便被标识为可读,这样一个accept()调用保证可以无阻塞完成。对其他套接口而言,可读性意味着有数据供读取,于是recv()或recvfrom()操作均能无阻塞完成
writeset:(可选)指针,指向一组等待可写性检查的套接口。如果一个套接口正在connect()连接(非阻塞),可写性意味着连接顺利建立。如果套接口并未处于connect()调用中,可写性意味着send()和sendto()调用将无阻塞完成
exceptset:(可选)指针,指向一组等待错误检查的套接口。
timeout:select()最多等待时间,对阻塞操作则为NULL
不需要查看的形参可以设为NULL
返回值:负值:select错误,正值:可读可写套接口数目,0:等待超时,没有可读写或错误的文件
判读:通过宏 FD_ISSET、&readset(宏参数) 和 具体某个描述符(宏参数) 判断该描述符是否可读;通过宏 FD_ISSET、&writeset(宏参数) 和 具体某个描述符(宏参数) 判断该描述符是否可写;
fcntl()
int fcntl(int s, int cmd, int val);
s 为要设置的socket,cmd 是要进行什么操作,val 是操作所需要的参数
返回值:成功取决于cmd,失败返回-1
函数有下面5种功能:
- 复制一个现有的描述符(cmd=F_DUPFD);
- 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD);
- 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL);
- 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN);
- 获得/设置记录锁(cmd=F_GETLK , F_SETLK或F_SETLKW);
在socket编程中使用 fcntl() 设置 sockfd 为非阻塞模式,则之后的connect、accept、recv、recvfrom等函数便失去了阻塞功能,变成了非阻塞函数。
用以下方法将socket设置为非阻塞方式:
int flags = fcntl(socket, F_GETFL, );
fcntl(socket, F_SETFL, flags | O_NONBLOCK);
将非阻塞的设置回阻塞可以用以下方式:
int flags = fcntl(socket, F_GETFL, );
fcntl(socket, F_SETFL, flags & ~O_NONBLOCK);
ioctl()
int ioctl(int s, long request, void *argp);
ioctl 是 input output control 的简写,控制I/O设备, 提供了一种获得设备信息和向设备发送控制参数的手段
返回值:成功返回0,出错返回-1
第一个参数:指示某个文件描述符(当然也包括 套接口描述符)
第二个参数:request 指示要ioctl执行的操作
第三个参数:总是某种指针,具体的指向类型依赖于 request 参数
我们可以把和网络相关的请求(request)划分为6 类:
套接口操作
文件操作
接口操作
ARP 高速缓存操作
路由表操作
流系统
下表列出了网络相关ioctl 请求的request 参数以及arg 地址必须指向的数据类型:
类别 |
Request |
说明 |
数据类型 |
套 |
SIOCATMARK |
是否位于带外标记 |
int |
文 |
FIONBIN |
设置/ 清除非阻塞I/O 标志 |
int |
接 |
SIOCGIFCONF |
获取所有接口的清单 |
struct ifconf |
ARP |
SIOCSARP |
创建/ 修改ARP 表项 |
struct arpreq |
路 |
SIOCADDRT |
增加路径 |
struct rtentry |
流 |
I_xxx |
inet_ntoa()、inet_addr()
inet_ntoa()将一个 32bits 无符号整数转换为点分十进制IP格式的字符串。inet_addr()正好相反,inet_addr可以判断形参是否IP格式的字符串,不是返回0xFFFFFFFF
char* inet_ntoa (struct in_addr addr);
结构体in_addr唯一一个成员就是32bits 无符号整数
inet_pton()
int inet_pton(int af, const char *src, void *dst);
af 是地址簇,比如 af=AF_INET,AF_INET(又称 PF_INET)是 IPv4 网络协议的套接字类型
指针 src 指向十进制IP格式的字符串
指针 dst 指向 32bits 无符号整数,即要得到的数据
IP4_ADDR()
IP4_ADDR(ipaddr, a,b,c,d)
ipaddr是一个结构体(struct ip_addr),含有成员变量addr
宏定义
PF_INET(AF_INET):IPv4(网际层)
PF_INET6(AF_INET6):IPv6(网际层)
SOCK_STREAM:设置套接字类型为流套接字,流套接字协议只有一个,即 IPPROTO_TCP(传输层)
SOCK_DGRAM:设置套接字类型为数据报套接字,数据报套接字协议有两个个,即 IPPROTO_UDP 和 IPPROTO_UDPLITE(传输层)
SOCK_RAW:设置套接字类型为原始套接字
原始套接字(SOCKET_RAW)允许对较低层次的协议直接访问,比如IP、 ICMP协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备,因为RAW SOCKET能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。比如,我们可以通过RAW SOCKET来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包。网络监听技术很大程度上依赖于SOCKET_RAW
套接字API是Unix网络的通用接口,允许使用各种网络协议和地址。
下面列出了一些例子,在现在的 Linux 和BSD中一般都已经实现了。
PF_LOCAL, PF_UNIX, PF_FILE
Local to host (pipes and file-domain)
PF_INET IP protocol family
PF_AX25 Amateur Radio AX.25
PF_IPX Novell Internet Protocol
PF_APPLETALK Appletalk DDP
PF_NETROM Amateur radio NetROM
PF_BRIDGE Multiprotocol bridge
PF_ATMPVC ATM PVCs
PF_X25 Reserved for X.25 project
PF_INET6 IP version 6
PF_ROSE Amateur Radio X.25 PLP
PF_DECnet Reserved for DECnet project
PF_NETBEUI Reserved for 802.2LLC project
PF_SECURITY Security callback pseudo AF
PF_KEY PF_KEY key management API
PF_NETLINK, PF_ROUTE
routing API
PF_PACKET Packet family
PF_ASH Ash
PF_ECONET Acorn Econet
PF_ATMSVC ATM SVCs
PF_SNA Linux SNA Project
PF_IRDA IRDA sockets
PF_PPPOX PPPoX sockets
PF_WANPIPE Wanpipe API sockets
PF_BLUETOOTH Bluetooth sockets