一、什么是socket
socket译为“插座”,在计算机通信领域,socket被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过这种方式,一台计算机可以接受其他计算机的数据,也可以向其他计算机发送数据。
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现。socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭).
说白了Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
注意:socket没有层的概念,它只是一个facade设计模式的应用,让编程变的更简单。是一个软件抽象层。在网络编程中,我们大量用的都是通过socket实现的。
二、socket编程流程
socket编程的总体流程如下图所示。 服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
三、socket相关函数介绍
Socket()
函数原型:
int socket(int protofamily, int type, int protocol)
该函数用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
参数:
Protofamily:
协议域,又称为协议族(family),即IP地址类型。常用的协议族有AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
注:AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。经常也用PF前缀,PF 是“Protocol Family”的简写,它和 AF 是一样的。例如,PF_INET 等价于 AF_INET,PF_INET6 等价于 AF_INET6。
type;
数据传输方式/套接字类型。常用的socket类型有,SOCK_STREAM(流格式套接字/面向连接的套接字 TCP)、SOCK_DGRAM(数据报套接字/无连接的套接字 UDP)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
protocol:
传输协议,常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。type和protocol不可以随意组合,如SOCK_STREAM不可以跟IPPROTO_UDP组合。
注:如果将该参数设置为0,系统会自动推演出应该使用什么协议,例如
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字
返回值:
NULL 创建失败
其他值 创建成功返回的socket描述符
实例:
#define AF_INET 2 //IPV4
#define SOCK_STREAM 1 //提供有序的,可靠的、双向的和基于连接的字节流,使用带外数据传输机制,TCP
int s = lwip_socket(AF_INET, SOCK_STREAM, 0); //创建SOCKET,IPV4,TCP,自动选择type类型对应的默认协议
If(s ==NULL)){
//失败
}
Bind()
该函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
函数原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
参数:
Sockfd:
即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
Addr:
一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:
typedef u32_t in_addr_t;
struct in_addr {
in_addr_t s_addr; //IP地址
};
struct sockaddr_in {
u8_t sin_len; //长度
sa_family_t sin_family; //地址族(address family),也就是地址类型
in_port_t sin_port; //16位端口号
struct in_addr sin_addr; //32位IP地址
#define SIN_ZERO_LEN 8
char sin_zero[SIN_ZERO_LEN]; //不使用,一般用0填充
};
Addrlen:
typedef u32_t socklen_t;
对应的地址的长度。
返回值:
0 成功
非0 失败
代码实例:
//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//创建sockaddr_in结构体变量
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
//将套接字和IP、端口绑定
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
sin_prot 为端口号。uint16_t 的长度为两个字节,理论上端口号的取值范围为 0~65536,但 0~1023 的端口一般由系统分配给特定的服务程序,例如 Web 服务的端口号为 80,FTP 服务的端口号为 21,所以我们的程序要尽量在 1024~65536 之间分配端口号。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,因为客户端在connect()时由系统随机生成一个。
这里有个问题,为什么要将sockaddr_in类型强制转换为sockaddr.
sockaddr结构体的定义如下:
struct sockaddr {
u8_t sa_len; //长度
sa_family_t sa_family; //地址族(address family),也就是地址类型
char sa_data[14]; //IP地址和端口号
};
将sockaddr与sockaddr_in对比(括号中的数字表示所占用的字节数)
sockaddr 和 sockaddr_in 的长度相同,都是16字节,只是将IP地址和端口号合并到一起,用一个成员 sa_data 表示。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“,遗憾的是,没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值,所以使用 sockaddr_in 来代替。这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。
可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。
实例:
#define AF_INET 2 //IPV4
#define CNET_AP_PORT 8686
/** 0.0.0.0 */
#define IPADDR_ANY ((u32_t)0x00000000UL)
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_len = sizeof(addr);
addr.sin_family = AF_INET; //IPV4
addr.sin_port = lwip_htons(CNET_AP_PORT); //端口
addr.sin_addr.s_addr = lwip_htonl(IPADDR_ANY); //地址
Int ret = lwip_bind(s, (struct sockaddr *)&addr, sizeof(addr)); //绑定
If(ret != 0){
//失败
}
Listen()
该函数可以让套接字进入被动监听状态,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
函数原型:
int listen(int sock, int backlog);
参数:
Sock:需要进入监听状态的套接字描述符
Backlog:请求队列的最大长度
当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。
返回值:
0 成功
非0 失败
注:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数
实例:
Int ret = lwip_listen(s, 5);
If(ret != 0){
//失败
}
lwip_setsockopt()
套接字描述符选项
函数原型:
int lwip_setsockopt (int s, int level, int optname, const void *optval, socklen_t optlen)
参数:
s:
套接字描述符
level:
选项定义的层次,支持一下层次:
- SOL_SOCKET,套接字层
- IPPROTO_TCP,TCP层
- IPPROTO_IP,IP层
- IPPROTO_IPV6,IPV6层
otpname:
需要设置的选项
在套接字级别上(SOL_SOCKET),option_name可以有以下取值:
#define SO_DEBUG 0x0001 /* turn on debugging info recording */
#define SO_DONTROUTE 0x0010 /* just use interface addresses */
#define SO_USELOOPBACK 0x0040 /* bypass hardware when possible */
#define SO_LINGER 0x0080 /* linger on close if data present */
#define SO_DONTLINGER ((int)(~SO_LINGER))
#define SO_OOBINLINE 0x0100 /* leave received OOB data in line */
#define SO_REUSEPORT 0x0200 /* allow local address & port reuse */
#define SO_SNDBUF 0x1001 /* send buffer size */
#define SO_RCVBUF 0x1002 /* receive buffer size */
#define SO_CONTIMEO 0x1009 /* connect timeout */
#define SO_NO_CHECK 0x100a /* don't create UDP checksum */
#define SO_BINDTODEVICE 0x100b /* bind to device */
#define SO_REUSEADDR 0x0004 /* Allow local address reuse */
#define SO_KEEPALIVE 0x0008 /* keep connections alive */
#define SO_BROADCAST 0x0020 /* permit to send and to receive broadcast messages (see IP_SOF_BROADCAST option) */
#define SO_ACCEPTCONN 0x0002 /* socket has had listen() */
#define SO_ERROR 0x1007 /* get error status and clear */
#define SO_SNDLOWAT 0x1003 /* send low-water mark */
#define SO_SNDTIMEO 0x1005 /* send timeout */
#define SO_RCVLOWAT 0x1004 /* receive low-water mark */
#define SO_RCVTIMEO 0x1006 /* receive timeout */
#define SO_TYPE 0x1008 /* get socket type */
SO_DEBUG,打开或关闭调试信息。BOOL
当option_value不等于0时,打开调试信息,否则,关闭调试信息。它实际所做的工作是在sock->sk->sk_flag中置 SOCK_DBG(第10)位,或清SOCK_DBG位。
SO_DONTROUTE,打开或关闭路由查找功能。BOOL
当option_value不等于0时,打开,否则,关闭。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_LOCALROUTE位。
SO_LINGER,延缓关闭。struct linger
如果选择此选项, close或 shutdown将等到所有套接字里排队的消息成功发送或到达延迟时间后>才会返回. 否则, 调用将立即返回。
该选项的参数(option_value)是一个linger结构:
struct linger {
int l_onoff; //开关
int l_linger; //延迟时间
};
如果linger.l_onoff值为0(关闭),则清 sock->sk->sk_flag中的SOCK_LINGER位;否则,置该位,并赋sk->sk_lingertime值为 linger.l_linger。
SO_DONTLINER ,不延缓关闭。BOOL
不要因为数据未发送就阻塞关闭操作。设置本选项相当于将SO_LINGER的l_onoff元素置为零。
SO_OOBINLINE,紧急数据放入普通数据流。BOOL
该操作根据option_value的值置或清sock->sk->sk_flag中的SOCK_URGINLINE位。
SO_SNDBUF,设置发送缓冲区的大小。INT
发送缓冲区的大小是有上下限的,其上限为256 * (sizeof(struct sk_buff) + 256),下限为2048字节。该操作将sock->sk->sk_sndbuf设置为val * 2,之所以要乘以2,是防止大数据量的发送,突然导致缓冲区溢出。最后,该操作完成后,因为对发送缓冲的大小作了改变,要检查sleep队列,如果有进程正在等待写,将它们唤醒。
SO_RCVBUF,设置接收缓冲区的大小。INT
接收缓冲区大小的上下限分别是:256 * (sizeof(struct sk_buff) + 256)和256字节。该操作将sock->sk->sk_rcvbuf设置为val * 2。
SO_NO_CHECK,打开或关闭校验和。BOOL
该操作根据option_value的值,设置sock->sk->sk_no_check。
SO_BINDTODEVICE,将套接字绑定到一个特定的设备上。BOOL
该选项最终将设备赋给sock->sk->sk_bound_dev_if。
SO_REUSEADDR,打开或关闭地址复用功能。BOOL
当option_value不等于0时,打开,否则,关闭。它实际所做的工作是置sock->sk->sk_reuse为1或0。
SO_KEEPALIVE,套接字保活。BOOL
如果协议是TCP,并且当前的套接字状态不是侦听(listen)或关闭(close),那么,当option_value不是零时,启用TCP保活定时 器,否则关闭保活定时器。对于所有协议,该操作都会根据option_value置或清 sock->sk->sk_flag中的 SOCK_KEEPOPEN位。
SO_BROADCAST,允许或禁止发送广播数据。BOOL
当option_value不等于0时,允许,否则,禁止。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_BROADCAST位。
SO_RCVTIMEO,设置接收超时时间。INT
该选项最终将接收超时时间赋给sock->sk->sk_rcvtimeo。
SO_SNDTIMEO,设置发送超时时间。INT
该选项最终将发送超时时间赋给sock->sk->sk_sndtimeo。
struct timeval timeout;
timeout.tv_sec = 30; //秒
timeout.tv_usec = 0; //微秒
if (setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout)) < 0) {
LOG_I(lwip_socket_example, "Setsockopt failed - set rcvtimeo\n");
}
在套接字级别上(IPPROTO_TCP),option_name可以有以下取值:
#define TCP_NODELAY 0x01 /* don't delay send to coalesce packets */
#define TCP_KEEPALIVE 0x02 /* send KEEPALIVE probes when idle for pcb->keep_idle milliseconds */
#define TCP_KEEPIDLE 0x03 /* set pcb->keep_idle - Same as TCP_KEEPALIVE, but use seconds for get/setsockopt */
#define TCP_KEEPINTVL 0x04 /* set pcb->keep_intvl - Use seconds for get/setsockopt */
#define TCP_KEEPCNT 0x05 /* set pcb->keep_cnt - Use number of probes sent for get/setsockopt */
#define TCP_MAXSEG 0x06 /* set maximum segment size */
TCP_NODELAY,不延迟发送。BOOL
TCP_KEEPALIVE,发送keepalive探测包,当在空闲时
TCP_KEEPIDLE,设置连接上如果没有数据发送时,多久发送keepalive探测分组,单位为秒。
TCP_KEEPINTVL,前后两次探测之间的时间间隔,单位是秒
TCP_KEEPCNT,最大重试次数。
int keepAlive = 1; // 非0值,开启keepalive属性
int keepIdle = 60; // 如该连接在60秒内没有任何数据往来,则进行此TCP层的探测
int keepInterval = 5; // 探测发包间隔为5秒
int keepCount = 3; // 尝试探测的最多次数
// 开启探活
setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepAlive, sizeof(keepAlive));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE, (void*)&keepIdle, sizeof(keepIdle));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPINTVL, (void *)&keepInterval, sizeof(keepInterval));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPCNT, (void *)&keepCount, sizeof(keepCount)
optval:
指针,指向存放选项待设置的新值的缓冲区
optlen:
optval缓冲区长度。
Accept()
当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求
函数原型:
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen)
参数:
Sock:接收到的客户端套接字
Addr:客户端的IP地址和端口号
Addrlen:长度
返回值:
-1 失败
>0 新监听的到客户端套接字描述符
注:后续和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。此外,accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。
实例:
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_len = sizeof(addr);
addr.sin_family = AF_INET; //IPV4
addr.sin_port = lwip_htons(CNET_AP_PORT); //端口
addr.sin_addr.s_addr = lwip_htonl(IPADDR_ANY); //地址
socklen_t sockaddr_len = sizeof(addr);
Int c = lwip_accept(s, (struct sockaddr *)&addr, &sockaddr_len); //等待新的连接
If(c < 0){
//异常
}
Connect()
客户端通过该函数与服务端建立链接
函数原型:
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
参数:
Sock:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。
serv_addr:要连接的IP和端口以及类型
Addrlen:长度
返回值:
0 成功
-1 失败
实例:
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_len = sizeof(addr);
addr.sin_family = AF_INET; //IPV4
addr.sin_port = lwip_htons(SOCK_TCP_SRV_PORT); //端口
inet_addr_from_ip4addr(&addr.sin_addr, netif_ip4_addr(sta_if)); //IP地址转换
ret = lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr));
If(ret < 0){
//失败
lwip_close(s);
}
Read()
从套接字中读取数据
函数原型:
ssize_t read(int fd, void *buf, size_t nbytes)
参数:
Fd:要读取的套接字描述符
Buf:要接收数据的缓冲区地址
Nbytes:要读取的数据的字节数
返回值:
-1 失败
其他值:成功则返回读取到的字节数,遇到文件结尾则返回0
实例:
uint8_t iobuf[512];
rlen = lwip_read(c, iobuf, sizeof(iobuf)); //读SOCKET数据
if (rlen < 0) {
//失败
}
Recv()
读取socket数据。
函数原型:
ssize_t read(int fd, void *buf, size_t nbytes, int flags)
参数:
Fd:
要读取的套接字描述符
Buf:
要接收数据的缓冲区地址
Nbytes:
要读取的数据的字节数
Flags:
0:
默认值,设置后该函数与read()相同。其他几个选项用到的很少。
MSG_PEEK:
查看可读的信息。数据被复制到缓冲区中,但不会从输入队列中删除。函数返回当前准备接受的字节数。
MSG_OOB:(out-of-band data)
指明是带外信息。带外数据是指连接双方的一方发生重要事情,想要迅速通知对方。这种通知在已经排队等待发送的任务“普通”数据之前发送。带外数据设计比普通数据有更高的优先级。
在网络上有两种类型的数据包,正常包和带外包。带外包可以通过检验一个TCP/IP包头的一个特定标志来决定。
MSG_WAITALL:
仅当发生以下事件之一时,接收请求才会完成:
- 调用方提供的缓冲区已完全满
- 连接已关闭
- 该请求已被取消或发生错误
注:这里MSG_WAITALL也只是尽量读全,在有中断的情况下,recv还是可能会被打断,造成没有读完指定的长度。所以即是是采用recv+MSG_WAITALL参数还是要考虑是否需要循环读取的问题。在实验中对于多数情况下recv+MSG_WAITALL还是可以读完数据的。所以相应的性能会比直接read进行循环读要好一些。
MSG_DONTWAIT:
- 如果发现没有数据就直接返回
- 如果发现有数据,采用有多少读多少的方式处理。
注:read完一次需要判断读到的数据长度再决定是否还需要再次读取。
返回值:
>0 接收到的数据大小
=0 对方调用了close API来关闭连接
<0 出错
EAGAIN:套接字已标记为非阻塞,而接收操作被阻塞或者接收超时
EBADF:sock不是有效的描述词
ECONNREFUSE:远程主机阻绝网络连接
EFAULT:内存空间访问出错
EINTR:操作被信号中断
EINVAL:参数无效
ENOMEM:内存不足
ENOTCONN:与面向连接关联的套接字尚未被连接上
ENOTSOCK:sock索引的不是套接字
实例:
rlen = lwip_read(c, iobuf, sizeof(iobuf),0); //读SOCKET数据
if (rlen < 0) {
//失败
}
Write()
向套接字写数据
函数原型:
ssize_t write(int fd, const void *buf, size_t nbytes);
参数:
Fd:要写入的套接字描述符
Buf:要写入的数据的缓冲区地址
Nbytes:要写入的数据的字节数
返回值:
-1 失败
其他值:成功则返回写入的字节数
实例:
char send_data[] = "Hello Server!";
ret = lwip_write(s, send_data, sizeof(send_data));
if (rlen < 0) {
//失败
}
Send()
向套接字发送数据
函数原型:
ssize_t read(int fd, void *buf, size_t nbytes, int flags)
参数:
Fd:
要发送的套接字描述符
Buf:
要发送的数据的缓冲区地址
Nbytes:
要发送的数据的字节数
Flags:
0:
默认值。设置后,与write()函数功能相同
MSG_DONTROUTE:
绕过路由表查找
MSG_DONTWAIT:
非阻塞操作
MSG_OOB:
发送带外数据
返回值:
>0 发送的字节数(实际上是拷贝到发送缓冲中的字节数)
=0 对方调用了close API来关闭连接
<0 发送失败
EBADF 参数s 非合法的socket处理代码
EFAULT 参数中有一指针指向无法存取的内存空间
ENOTSOCK 参数s为一文件描述词,非socket
EINTR 被信号所中断
EAGAIN 此操作会令进程阻断,但参数s的socket为不可阻断
ENOBUFS 系统的缓冲内存不足
ENOMEM 核心内存不足
EINVAL 传给系统调用的参数不正确
实例:
char send_data[] = "Hello Server!";
ret = lwip_send(s, send_data, sizeof(send_data),0);
if (rlen < 0) {
//失败
}
Close()
关闭之前打开的套接字
函数原型:
int close(int fd);
参数:
Fd:要关闭的套接字描述符
返回值:
0 成功
其他值:失败
close一个TCP socket的缺省行为时把该socket标记为已关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求
实例:
lwip_close(s);
Select()
在一段指定的时间内,监听文件描述符上可读、可写和异常等事件。用select可以完成非阻塞,进程或线程执行到此函数时,不必非要等待事件的发生,会根据select返回结果来反映执行情况。
函数原型:
int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
参数:
Maxfdp:
被监听的文件描述符的总数,它比所有文件描述符集合中的文件描述符的最大值大1,因为文件描述符是从0开始计数的。
Readset:
可读文件描述符集合
Writeset:
可写文件描述符集合
Exceptset:
异常事件文件描述集合
Timeout:
设置超时时间。NULL表示无线等待,类似于阻塞。
timeval结构体:
struct timeval
{
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
};
返回值:
0 超时
-1 失败
>0 表示就绪描述符的数目
查看该函数底层源码,发现如下函数:
查看源码:
u32_t sys_arch_sem_wait(sys_sem_t *pxSemaphore, u32_t ulTimeout)
{
TickType_t xStartTime, xEndTime, xElapsed;
unsigned long ulReturn;
xStartTime = xTaskGetTickCount();
if (ulTimeout != 0UL) {
if (xSemaphoreTake( *pxSemaphore, ulTimeout / portTICK_PERIOD_MS ) == pdTRUE) {
xEndTime = xTaskGetTickCount();
xElapsed = (xEndTime - xStartTime) * portTICK_PERIOD_MS;
ulReturn = xElapsed;
} else {
ulReturn = SYS_ARCH_TIMEOUT;
}
} else {
while (xSemaphoreTake( *pxSemaphore, portMAX_DELAY ) != pdTRUE)
;
xEndTime = xTaskGetTickCount();
xElapsed = (xEndTime - xStartTime) * portTICK_PERIOD_MS;
if (xElapsed == 0UL) {
xElapsed = 1UL;
}
ulReturn = xElapsed;
}
return ulReturn;
}
可以看到,该函数创建了一个信号量,然后等待信号量的到来。所以select函数的等待是将线程直接挂起。此时会让出CPU使用权给其他的线程使用。
以下介绍与select函数相关的常见的几个宏
#include <sys/select.h>
int FD_ZERO(int fd, fd_set *fdset); //一个 fd_set类型变量的所有位都设为 0
int FD_CLR(int fd, fd_set *fdset); //清除某个位时可以使用
int FD_SET(int fd, fd_set *fd_set); //设置变量的某个位置位
int FD_ISSET(int fd, fd_set *fdset); //测试某个位是否被置位
select使用范例:
当声明了一个文件描述符集后,必须用FD_ZERO将所有位置零。之后将我们所感兴趣的描述符所对应的位置位,操作如下:
fd_set rset;
int fd;
FD_ZERO(&rset);
FD_SET(fd, &rset);
然后调用select函数,拥塞等待文件描述符事件的到来;如果超过设定的时间,则不再等待,继续往下执行。
select(fd+1, &rset, NULL, NULL,NULL); //一直阻塞等待
select返回后,用FD_ISSET测试给定位是否置位:
if(FD_ISSET(fd, &rset)
{
//检测到数据
}
select模型的关键是使用一种有序的方式,对多个套接字进行统一管理与调度 。
如上所示,用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。
从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
实例:
fd_set fd;
struct timeval timeout;
timeout.tv_sec = 60; //秒
timeout.tv_usec = 0; //微秒
FD_ZERO( &fd ); //一个df_set类型变量的所有位都设为0,将套接字集合清空
FD_SET( sock, &fd ); //设置变量的某个位。加入你感兴趣的套接字到集合,这里是一个读数据的套接字
nRet = lwip_select( sock+1, &fd, (fd_set *)NULL, (fd_set *)NULL, &timeout );
if ( nRet > 0 ) {
if ( FD_ISSET(sock, &fd) > 0 ){
//读socket数据
}
}
Htonl() htons() ntohl() ntohs()
在编程的时候,往往会遇到自己的网络顺序和主机顺序的问题。这时就需要以上四个函数进行调节了。
htonl()--"Host to Network Long"
ntohl()--"Network to Host Long"
htons()--"Host to Network Short"
ntohs()--"Network to Host Short"
主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:
a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是大端存储。请谨记务必将主机字序转化为网络字节序再赋给socket。