socket编程 ------ BSD socket API

时间:2021-09-12 08:38:03

伯克利套接字Berkeley sockets),也称为BSD Socket。伯克利套接字的应用编程接口(API)是采用C语言的进程间通信的库,经常用在计算机网络间的通信。 BSD Socket的应用编程接口已经是网络套接字的抽象标准。大多数其他程序语言使用一种相似的编程接口。它最初是由加州伯克利大学为Unix系统开发出来的。所有现代的操作系统都实现了伯克利套接字接口,因为它已经是连接互联网的标准接口了。

API函数

以下函数是最基本的 socket API

  • socket() 创造某种类型的套接字,分配一些系统资源,用返回的整数识别。
  • bind() 一般是用在服务器这边,和一个套接字地址结构相连,比如说是一个特定的本地端口号和一个IP地址。
  • listen()用在服务器一边,导致一个绑定的TCP套接字进入监听状态。
  • connect() 用在客户机这边,给套接字分配一个空闲的端口号。比如说一个TCP套接字,它会试图建立一个新的TCP连接。
  • accept() 用在服务器这边。从客户机那接受请求试图创造一个新的TCP连接,并把一个套接字和这个连接相联系起来。
  • send() and recv(), or write() and read(), or sendto() and recvfrom()用来接收和发送数据。
  • close() 当套接字的引用计数为0的时候才会引发TCP的四次挥手,关闭连接,系统释放资源。------- 不可对某个socket连续调用两次close,否则第二次调用会出现释放未分配的内存问题(野指针)(在LWIP下测试得出的结论)。个人想法:应该在close函数里面把socket置成某个数,这样每次进入close,如果socket等于某个数,表示已经close过,直接函数返回。
  • shutdown()  不用管套接字的引用计数,调用读写函数返回小于0的数,退出阻塞。

以下函数用来设置 / 获取套接字的属性,有些函数的功能有重叠;fcntl() 和 ioctl() 功能比较强大,在linux c 中不仅仅用来网络编程

  • gethostbyname() and gethostbyaddr()用来解析主机名和地址。
  • 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

这四个函数用在 TCP 通信中
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_TCPIPPROTO_UDPIPPROTO_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_STREAMSOCK_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);

socket编程 ------ BSD socket API

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 
SIOCSPGRP 
SIOCGPGRP

是否位于带外标记 
设置套接口的进程ID 或进程组ID 
获取套接口的进程ID 或进程组ID

int 
int 
int

 

FIONBIN 
FIOASYNC 
FIONREAD 
FIOSETOWN 
FIOGETOWN

设置/ 清除非阻塞I/O 标志 
设置/ 清除信号驱动异步I/O 标志 
获取接收缓存区中的字节数 
设置文件的进程ID 或进程组ID 
获取文件的进程ID 或进程组ID

int 
int 
int 
int 
int

 

SIOCGIFCONF 
SIOCSIFADDR 
SIOCGIFADDR 
SIOCSIFFLAGS 
SIOCGIFFLAGS 
SIOCSIFDSTADDR 
SIOCGIFDSTADDR 
SIOCGIFBRDADDR 
SIOCSIFBRDADDR 
SIOCGIFNETMASK 
SIOCSIFNETMASK 
SIOCGIFMETRIC 
SIOCSIFMETRIC 
SIOCGIFMTU 
SIOCxxx

获取所有接口的清单 
设置接口地址 
获取接口地址 
设置接口标志 
获取接口标志 
设置点到点地址 
获取点到点地址 
获取广播地址 
设置广播地址 
获取子网掩码 
设置子网掩码 
获取接口的测度 
设置接口的测度 
获取接口MTU 
(还有很多取决于系统的实现)

struct ifconf 
struct ifreq 
struct ifreq 
struct ifreq 
struct ifreq 
struct ifreq 
struct ifreq 
struct ifreq 
struct ifreq 
struct ifreq 
struct ifreq 
struct ifreq 
struct ifreq 
struct ifreq

ARP

SIOCSARP 
SIOCGARP 
SIOCDARP

创建/ 修改ARP 表项 
获取ARP 表项 
删除ARP 表项

struct arpreq 
struct arpreq 
struct arpreq

 

SIOCADDRT 
SIOCDELRT

增加路径 
删除路径

struct rtentry 
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