最近接了一个第三方的C库。是直接在native层创建socket通信的。
之前只了解java层的阻塞模式socket。于是初看到c端的socket写法非常困惑。最后发现是线程模型不一样,c端socket类似于 Java端的NIO(非阻塞模式)。
首先是File Description,简写FD,它是Linux特有的东西,类似于windows的句柄
Linux实现非阻塞式Socket靠的是一个struct和几个函数:
1. fd_set
typedef struct {
fd_mask fds_bits[FD_SETSIZE/NFDBITS];
} fd_set;
fd_set是一个结构体,成员只有一个unsigned long类型的数组。 fd_mask是unsigned long。
我把fd_set称为观察数组。观察的是句柄所指向的内容, 当发生改变后就会通知你。
数组成员一一对应一个文件句柄(socket、文件、管道、设备等)建立联系。调用FD_SET()建立联系。
2. FD_ZERO()
FD_ZERO就是把fd_set这个结构体初始化为0。
/* Inline loop so we don't have to declare memset. */
#define FD_ZERO(fd_set)
do {
size_t __i;
for (__i = 0; __i < sizeof(fd_set)/sizeof(fd_mask); ++__i) { \
(fd_set)->fds_bits[__i] = 0;
}
} while (0)
将fd_set观察数组初始化清零。就像memset()一样.
3. FD_SET()
把需要观察的socket fd加入到fd_set的观察数组。
4. FD_CLR()
从fd_set观察数组中移除。在这次socket编程中没有用到。
5. select()
int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout);
这是实现非阻塞式socket最关键的函数。
当把需要观察的socket加入到fd_set以后。就开始循环调用 select()。当调用select()时,Linux内核会根据IO状态修改fd_set的内容,由此来通知哪个句柄发生了改变,有可读内容或有可写的机会了。一定要设置超时时间,一般是5ms。不然等待时间太长,thread就无法顺利结束。这里犯过错。
1.timeout=NULL(阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
2.timeout所指向的结构设为非零时间(等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)
3.timeout所指向的结构,时间设为0(非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
参数maxfd是需要监视的最大的文件描述符值+1;rdset是可读文件描述符的观察数组;wrset是可写文件描述符的观察数组,以及异常文件描述符的观察数组。
6. FD_ISSET()
当select()函数返回有改变时,调用FD_ISSET来确定发送改变的就是你关心的socket fd。
7. accept()
这个函数就建立server socket的时候使用的,类似于java的accept方法,阻塞等待客户端的scoket过来连接。但是这儿要讲的accept函数是不阻塞的。通过循环调用select来通知有客户端来建立连接, FD_ISSUT()函数来确定是客户端发过来的连接请求。
8.recv()
接收客户端或者服务端的数据。如何有可接收的数据,也是通过循环调用select函数来通知你的。再通过FD_ISSUT()函数来确定就是你关心的那个客户端或服务端。
下面是简单的流程:
static THREAD_RETVAL server_socket_thread(void *arg) {
server_socket *server_socket = arg;
int stream_fd = -1;
unsigned char packet[128];
memset(packet, 0, 128);
unsigned int readstart = 0;
while (1) {
fd_set rfds;
struct timeval tv;
int nfds, ret;
MUTEX_LOCK(server_socket->run_mutex);
if (!server_socket->running) {
MUTEX_UNLOCK(server_socket->run_mutex);
break;
}
MUTEX_UNLOCK(server_socket->run_mutex);
//设置超时时间5ms
tv.tv_sec = 0;
tv.tv_usec = 5000;
//rfds清零
FD_ZERO(&rfds);
if (stream_fd == -1) {
//server_socket->data_sock是做为服务端的socket句柄。放到观察数组里去
//以此来监听是否有客户端来建立连接。
FD_SET(server_socket->data_sock, &rfds);
nfds = server_socket->data_sock + 1; //最新监听句柄加一,just规则。
} else {
FD_SET(stream_fd, &rfds);
nfds = stream_fd + 1;
}
//检查是否有变化的fd, 这里的变化是有可读可写等等。
ret = select(nfds, &rfds, NULL, NULL, &tv);
if (ret == 0) {
/* Timeout happened */
continue;
} else if (ret == -1) {
//出现异常需要关闭
logger_log(server_socket->logger, LOGGER_INFO, "Error in select");
break;
}
//判断是否有客户端过来建立连接
if (stream_fd == -1 && FD_ISSET(server_socket->data_sock, &rfds)) {
struct sockaddr_storage saddr;
socklen_t saddrlen;
logger_log(server_socket->logger, LOGGER_INFO, "Accepting client");
saddrlen = sizeof(saddr);
stream_fd = accept(server_socket->data_sock, (struct sockaddr *) &saddr,
&saddrlen);
if (stream_fd == -1) {
//出现异常并关闭整个线程。
logger_log(server_socket->logger, LOGGER_INFO, "Error in accept %d %s", errno,
strerror(errno));
break;
}
}
//这里就是读取客户端的数据了。
if (stream_fd != -1 && FD_ISSET(stream_fd, &rfds)) {
// packetlen初始0
ret = recv(stream_fd, packet + readstart, 4 - readstart, 0);
if (ret == 0) {
/* TCP socket closed */
logger_log(server_socket->logger, LOGGER_INFO, "TCP socket closed");
break;
} else if (ret == -1) {
/* FIXME: Error happened */
logger_log(server_socket->logger, LOGGER_INFO, "Error in recv");
break;
}
readstart += ret;
if (readstart < 4) {
continue;
}
// 普通数据块
do {
// 读取剩下的124字节
ret = recv(stream_fd, packet + readstart, 128 - readstart, 0);
readstart = readstart + ret;
} while (readstart < 128);
//-----------------
在这里接受处理客户端数据,主要是根据协商好的协议读出数据
//-----------------
memset(packet, 0, 128);
readstart = 0;
}
}
//关闭server socket
if (stream_fd != -1) {
closesocket(stream_fd);
}
logger_log(server_socket->logger, LOGGER_INFO, "Exiting TCP server_socket_thread ");
return 0;
}
鉴于对Linux的Socket实现方式, JAVA NIO的原理就很好理解了,背后就是用了select模式。
最后再说一下阻塞模式和非阻塞模式:
1. 阻塞模式, 有一个缺点,就是严重浪费线程资源。服务端需要一对一的开启线程,给每一个客户端来处理数据交换。
2. 服务端和客户端连接后,并不是时时刻刻都有数据交换的,所以socket管道本身时间利用率就不高,完全可以通过不断查询客户端socket有没有可读可写的变化,再来处理读写。
3. 阻塞模式没有fd系列方法, 也没有select方法。只有accept(), rec()这两个阻塞的方法,有连接请求或者有数据可读时才返回。而非阻塞模式只要一条线程就可以处理很多客户端的连接。只要用select()不断的查询IO变化就可以了。