socket编程网络通信
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
网络编程之SOCKET
Socket在所有网络操作系统和网络应用程序中都是必不可少的,它是网络通信中应用进程和网络协议之间的接口。在Linux操作系统中,socket属于文件系统的一部分,网络通信可以看作是对文件的读取。这就使得用户对网络的控制像对文件的控制一样方便。
要了解socket就必须要了解一些基本的概念,如:套接口、网络编程的结构等。下面分别讲述这些概念。
一、基本概念:
1.套接口:
简单地说,套接口就是一种使用UNIX系统中的文件描述符和系统进程通信的一种方法。因为在UNIX系统中,所有的I/O操作都是通过读写文件描述符而产生的。文件描述符就是一个和打开的文件相关连的整数。但文件可以是一个网络连接、一个FIFO、一个管道、一个终端、一个真正存储在磁盘上的文件或者UNIX系统中的任何其他的东西。所以,如果你希望通过Internet和其他的程序进行通信,你只有通过文件描述符。使用系统调用socket(),你可以得到socket()描述符。然后你可以使用send()和recv()调用而与其他的程序通信。你也可以使用一般的文件操作来调用read()和write()而与其他的程序进行通信,但send()和recv()调用可以提供一种更好的数据通信的控制手段。
2.Internet套接口
有两种最常用的Internet套接口,“数据流套接口”和“数据报套接口”,我们用“SOCK_STREAM”和“SOCK_DGRAM”分别代表上面两种套接口。数据报套接口有时也叫做“无连接的套接口”。数据流套接口是可靠的双向连接的通信数据流。如果你在套接口中以“ 1, 2”的顺序放入两个数据,它们在另一端也会以“1, 2”的顺序到达。它们也可以被认为是无错误的传输。经常使用的telnet应用程序就是使用数据流套接口的一个例子。使用HTTP的WWW浏览器也使用数据流套接口来读取网页。事实上,如果你使用telnet登录到一个WWW站点的80端口,然后键入“GET网页名”,你将可以得到这个HTML页。数据流套接口使用TCP得到这种高质量的数据传输。数据报套接口使用UDP,所以数据报的顺序是没有保障的。数据报是按一种应答的方式进行数据传输的。
3.数据结构
下面我们要讨论使用套接口编写程序可能用到的数据结构。
struct sockaddr
{
unsigned short int sa_family;
char sa_data[14];
};
sa_family 为调用socket()时的domain参数,即AF_xxxx值。
sa_data最多使用14个字符长度。
此sockaddr结构会因使用不同的socket domain而有不同结构定义,例如使用AF_INET domain,其socketaddr结构定义便为:
struct socketaddr_in
{
unsigned short int sin_family;
uint16_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
struct i
n_addr
{
uint32_t s_addr;
};
sin_family 即为sa_family
sin_port 为使用的port编号
sin_addr.s_addr 为IP地址
sin_zero 填充0以保持与struct sockaddr大小一致。
这个数据结构使得使用其中的各个元素更为方便。要注意的是sin_zero应该使用bzero() 或者memset()而设置为全0。另外,一个指向sockaddr_in数据结构的指针可以投射到一个指向数据结构sockaddr的指针,反之亦然。
4.网络字节顺序
一个网络可能由不同的体系结构的CPU组成,这些不同体系的CPU使用的字节顺序不同,有的CPU使用big_endian(大端,在存储器中高字节存储在后),有的CPU使用little_endian(小端,在存储器中高字节存储在前)。因此为了在网络间能够进行数据交换,需要对这些不同的字节顺序进行处理。
下面是几个字节顺序转换函数:
htons():表示“Host to Network Short”,把主机地址字节顺序转向网络字节顺序(对短整型数据操作)。
htonl():表示“Host to Network Long”,把主机地址字节顺序转向网络字节顺序(对长整型数据操作)。
ntohs():表示“Network to Host Short”,把网络字节顺序转向主机地址字节顺序(对短整型数据操作)。
ntohl():表示“Network to Host Short”,把网络字节顺序转向主机地址字节顺序(对长整型数据操作)。
二、SOCKET通信常用API
1.socket(建立一个socket通信) 相关函数 accept,bind,connect,listen
表头文件
#include
#include
定义函数
int socket(int domain,int type,int protocol);
函数说明
socket()用来建立一个新的socket,也就是向系统注册,通知系统建立一通信端口。参数domain 指定使用何种的地址类型,完整的定义在/usr/include/bits/socket.h 内,底下是常见的协议:
PF_UNIX/PF_LOCAL/AF_UNIX/AF_LOCAL UNIX 进程通信协议
PF_INET?AF_INET Ipv4网络协议
PF_INET6/AF_INET6 Ipv6 网络协议
PF_IPX/AF_IPX IPX-Novell协议
PF_NETLINK/AF_NETLINK 核心用户接口装置
PF_X25/AF_X25 ITU-T X.25/ISO-8208 协议
PF_AX25/AF_AX25 业余无线AX.25协议
PF_ATMPVC/AF_ATMPVC 存取原始ATM PVCs
PF_APPLETALK/AF_APPLETALK appletalk(DDP)协议
PF_PACKET/AF_PACKET 初级封包接口
参数
type有下列几种数值:
SOCK_STREAM 提供双向连续且可信赖的数据流,即TCP。支持
OOB 机制,在所有数据传送前必须使用connect()来建立连线状态。
SOCK_DGRAM 使用不连续不可信赖的数据包连接
SOCK_SEQPACKET 提供连续可信赖的数据包连接
SOCK_RAW 提供原始网络协议存取
SOCK_RDM 提供可信赖的数据包连接
SOCK_PACKET 提供和网络驱动程序直接通信。
protocol用来指定socket所使用的传输协议编号,通常此参考不用管它,设为0即可。
返回值
成功则返回socket处理代码,失败返回-1。
错误代码
E
PROTONOSUPPORT 参数domain指定的类型不支持参数type或protocol指定的协议
ENFILE 核心内存不足,无法建立新的socket结构
EMFILE 进程文件表溢出,无法再建立新的socket
EACCESS 权限不足,无法建立type或protocol指定的协议
ENOBUFS/ENOMEM 内存不足
EINVAL 参数domain/type/protocol不合法
2.bind(对socket定位) 相关函数 socket,accept,connect,listen
表头文件
#include
#include
定义函数
int bind(int sockfd,struct sockaddr * my_addr,int addrlen);
函数说明
bind()用来设置给参数sockfd的socket一个名称。
参数
addrlen为sockaddr的结构长度。
返回值
成功则返回0,失败返回-1,错误原因存于errno中。
错误代码
EBADF 参数sockfd 非合法socket处理代码。
EACCESS 权限不足
ENOTSOCK 参数sockfd为一文件描述词,非socket。
3. connect(建立socket连线) 相关函数 socket,bind,listen
表头文件
#include
#include
定义函数
int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
函数说明
connect()用来将参数sockfd 的socket 连至参数serv_addr 指定的网络地址。结构sockaddr请参考bind()。参数addrlen为sockaddr的结构长度。
返回值
成功则返回0,失败返回-1,错误原因存于errno中。
错误代码
EBADF 参数sockfd 非合法socket处理代码
EFAULT 参数serv_addr指针指向无法存取的内存空间
ENOTSOCK 参数sockfd为一文件描述词,非socket。
EISCONN 参数sockfd的socket已是连线状态
ECONNREFUSED 连线要求被server端拒绝。
ETIMEDOUT 企图连线的操作超过限定时间仍未有响应。
ENETUNREACH 无法传送数据包至指定的主机。
EAFNOSUPPORT sockaddr结构的sa_family不正确。
EALREADY socket为不可阻断且先前的连线操作还未完成。
范例
/* 此程序会连线TCP server,并将键盘输入的字符串传送给server。*/
#include
#include
#include
#include
#include
#include
#include
#define PORT 1234
#define SERVER_IP “127.0.0.1”
int main(void)
{
int s;
struct sockaddr_in addr;
char buffer[256];
if((s = socket(AF_INET,SOCK_STREAM,0))<0)
{
perror(“socket”);
exit(1);
}
/* 填写sockaddr_in结构*/
bzero(&addr,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port=htons(PORT);
addr.sin_addr.s_addr = inet_addr(SERVER_IP);
/* 尝试连线*/
if(connect(s,&addr,sizeof(addr))<0)
{
perror(“connect”);
exit(1);
}
/* 接收由server端传来的信息*/
recv(s,buffer,sizeof(buffer),0);
printf(“%s\n”,buffer);
while(1)
{
bzero(buffer,sizeof(buffer));
/* 从标准输入设备取得字符串*/
read(STDIN_FILENO,buffer,sizeof(buffer));
/* 将字符串传给serv
er端*/
if(send(s,buffer,sizeof(buffer),0)<0)
{
perror(“send”);
exit(1);
}
}
}
执行
$ ./connect
Welcome to server!
hi I am client! /*键盘输入*/
/*
4. accept(接受socket连线) 相关函数 socket,bind,listen,connect
表头文件
#include
#include
定义函数
int accept(int s,struct sockaddr * addr,int * addrlen);
函数说明
accept()用来接受参数s的socket连线。参数s的socket必需先经bind()、listen()函数处理过,当有连线进来时 accept()会返回一个新的socket处理代码,往后的数据传送与读取就是经由新的socket处理,而原来参数s的socket能继续使用 accept()来接受新的连线要求。连线成功时,参数addr所指的结构会被系统填入远程主机的地址数据,参数addrlen为scokaddr的结构 长度。关于结构sockaddr的定义请参考bind()。
返回值
成功则返回新的socket处理代码,失败返回-1,错误原因存于errno中。
错误代码
EBADF 参数s 非合法socket处理代码。
EFAULT 参数addr指针指向无法存取的内存空间。
ENOTSOCK 参数s为一文件描述词,非socket。
EOPNOTSUPP 指定的socket并非SOCK_STREAM。
EPERM 防火墙拒绝此连线。
ENOBUFS 系统的缓冲内存不足。
ENOMEM 核心内存不足。
5. listen(等待连接) 相关函数 socket,bind,accept,connect
表头文件
#include
定义函数
int listen(int s,int backlog);
函数说明
listen()用来等待参数s 的socket连线。参数backlog指定同时能处理的最大连接要求,如果连接数目达此上限则client端将收到ECONNREFUSED的错误。 Listen()并未开始接收连线,只是设置socket为listen模式,真正接收client端连线的是accept()。通常listen()会 在socket(),bind()之后调用,接着才调用accept()。
返回值
成功则返回0,失败返回-1,错误原因存于errno
附加说明
listen()只适用SOCK_STREAM或SOCK_SEQPACKET的socket类型。如果socket为AF_INET则参数backlog 最大值可设至128。
错误代码
EBADF 参数sockfd非合法socket处理代码
EACCESS 权限不足
EOPNOTSUPP 指定的socket并未支援listen模式。
范例
#include
#include
#include
#include
#include
#define PORT 1234
#define MAXSOCKFD 10
int main(void)
{
int sockfd,newsockfd,is_connected[MAXSOCKFD],fd;
struct sockaddr_in addr;
int addr_len = sizeof(struct sockaddr_in);
fd_set readfds;
char buffer[256];
char msg[ ] =”Welcome to server!”;
if ((sockfd = socket(AF_INET,SOCK_STREAM,0))<0)
{
perror(“socket”);
exit(1);
}
bzero(&addr,sizeof(addr));
addr.sin_family =AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(soc
kfd,&addr,sizeof(addr))<0)
{
perror(“connect”);
exit(1);
}
if(listen(sockfd,3)<0)
{
perror(“listen”);
exit(1);
}
for(fd=0;fd
is_connected[fd]=0;
while(1)
{
FD_ZERO(&readfds);
FD_SET(sockfd,&readfds);
for(fd=0;fd
{
if(is_connected[fd])
FD_SET(fd,&readfds);
}
if(!select(MAXSOCKFD,&readfds,NULL,NULL,NULL))
continue;
for(fd=0;fd
{
if(FD_ISSET(fd,&readfds))
{
if(sockfd = =fd)
{
if((newsockfd = accept (sockfd,&addr,&addr_len))<0)
perror(“accept”);
write(newsockfd,msg,sizeof(msg));
is_connected[newsockfd] =1;
printf(“cnnect from %s\n”,inet_ntoa(addr.sin_addr));
}
else
{
bzero(buffer,sizeof(buffer));
if(read(fd,buffer,sizeof(buffer))<=0)
{
printf(“connect closed.\n”);
is_connected[fd]=0;
close(fd);
}
else
printf(“%s”,buffer);
}
}
}
}
执行
$ ./listen
connect from 127.0.0.1
hi I am client
connected closed.
6. select()
select()系统调用可以使进程检测同时等待的多个I/O设备,当没有设备准备好时,select()阻塞,其中任一设备准备好时,select()就返回。
select()的调用形式为:
#include
#include
int select(int maxfd, fd_set *readfds, fd_set *writefds, fe_set *exceptfds, const struct timeval *timeout);
参数:
select 的第一个参数是文件描述符集中要被检测的比特数,这个值必须至少比待检测的最大文件描述符大1;参数readfds指定了被读监控的文件描述符集;参数 writefds指定了被写监控的文件描述符集;而参数exceptfds指定了被例外条件监控的文件描述符集。
参数timeout起了定时器的作用:到了指定的时间,无论是否有设备准备好,都返回调用。timeval的结构定义如下:
struct timeval{
long tv_sec; //表示几秒
long tv_usec; //表示几微妙
}
timeout取不同的值,该调用就表现不同的性质:
1.timeout为0,调用立即返回;
2.timeout为NULL,select()调用就阻塞,直到知道有文件描述符就绪;
3.timeout为正整数,就是一般的定时器。
select调用返回时,除了那些已经就绪的描述符外,select将清除readfds、writefds和exceptfds中的所有没有就绪的描述符。select的返回值有如下情况:
1.正常情况下返回就绪的文件描述符个数;
2.经过了timeout时长后仍无设备准备好,返回值为0;
3.如果select被某个信号中断,它将返回-1并设置errno为EINTR。
4.如果出错,返回-1并设置相应的errno。
8.send(经socket传送数据) 相关函数 sendto,sendmsg,recv,recvfrom,socket
表头文件
#include
#include
定义函数
int send(int s,const void * msg,int len,unsigned int falgs);
函数说明
send()用来将数据由指定的socket 传给对方主机。参数s为已建立好连接的socket。参数msg指向欲连线的数据内容,参数len则为数据长度。参数flags一般设0,其他数值定义如下
MSG_OOB 传送的数据以out-of-band 送出。
MSG_DONTROUTE 取消路由表查询
MSG_DONTWAIT 设置为不可阻断运作
MSG_NOSIGNAL 此动作不愿被SIGPIPE 信号中断。
返回值
成功则返回实际传送出去的字符数,失败返回-1。错误原因存于errno
错误代码
EBADF 参数s 非合法的socket处理代码。
EFAULT 参数中有一指针指向无法存取的内存空间
ENOTSOCK 参数s为一文件描述词,非socket。
EINTR 被信号所中断。
EAGAIN 此操作会令进程阻断,但参数s的socket为不可阻断。
ENOBUFS 系统的缓冲内存不足
ENOMEM 核心内存不足
EINVAL 传给系统调用的参数不正确。
9.recv(经socket接收数据) 相关函数 recvfrom,recvmsg,send,sendto,socket
表头文件
#include
#include
定义函数
int recv(int s,void *buf,int len,unsigned int flags);
函数说明
recv()用来接收远端主机经指定的socket传来的数据,并把数据存到由参数buf 指向的内存空间,参数len为可接收数据的最大长度。
参数
flags一般设0。其他数值定义如下:
MSG_OOB 接收以out-of-band 送出的数据。
MSG_PEEK 返回来的数据并不会在系统内删除,如果再调用recv()会返回相同的数据内容。
MSG_WAITALL强迫接收到len大小的数据后才能返回,除非有错误或信号产生。
MSG_NOSIGNAL此操作不愿被SIGPIPE信号中断返回值成功则返回接收到的字符数,失败返回-1,错误原因存于errno中。
错误代码
EBADF 参数s非合法的socket处理代码
EFAULT 参数中有一指针指向无法存取的内存空间
ENOTSOCK 参数s为一文件描述词,非socket。
EINTR 被信号所中断
EAGAIN 此动作会令进程阻断,但参数s的socket为不可阻断
ENOBUFS 系统的缓冲内存不足。
ENOMEM 核心内存不足
EINVAL 传给系统调用的参数不正确。
10.read(由已打开的文件读取数据) 相关函数 readdir,write,fcntl,close,lseek,readlink,fread
表头文件
#include
定义函数
ssize_t read(int fd,void * buf ,size_t count);
函数说明
read()会把参数fd 所指的文件传送count个字节到buf指针所指的内存中。若参数count为0,则read()不会有作用并返回0。返回值为实际读取到的字节数,如果返回0,表示已到达文件尾或是无可读取的数据,此外文件读写位置会随读取到的字节移动。
附加说明 如果顺利read()会返回实际读到的字节数,最好能将返回值与参数count 作比较,若返回的字节数比要求读取的字节数
少,则有可能读到了文件尾、从管道(pipe)或终端机读取,或者是read()被信号中断了读取动作。当有错误发生时则返回-1,错误代码存入errno中,而文件读写位置则无法预期。
错误代码
EINTR 此调用被信号所中断。
EAGAIN 当使用不可阻断I/O 时(O_NONBLOCK),若无数据可读取则返回此值。
EBADF 参数fd 非有效的文件描述词,或该文件已关闭。
11.write(将数据写入已打开的文件内) 相关函数 open,read,fcntl,close,lseek,sync,fsync,fwrite
表头文件
#include
定义函数
ssize_t write (int fd,const void * buf,size_t count);
函数说明
write()会把参数buf所指的内存写入count个字节到参数fd所指的文件内。当然,文件读写位置也会随之移动。
返回值
如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中。
错误代码
EINTR 此调用被信号所中断。
EAGAIN 当使用不可阻断I/O 时(O_NONBLOCK),若无数据可读取则返回此值。
EADF 参数fd非有效的文件描述词,或该文件已关闭。
补充:文件描述符
系统提供了4个宏对描述符集进行操作:
#include
#include
void FD_SET(int fd, fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
void FD_ISSET(int fd, fd_set *fdset);
void FD_ZERO(fd_set *fdset);
宏FD_SET 设置文件描述符集fdset中对应于文件描述符fd的位(设置为1),宏FD_CLR清除文件描述符集fdset中对应于文件描述符fd的位(设置为 0),宏FD_ZERO清除文件描述符集fdset中的所有位(既把所有位都设置为0)。使用这3个宏在调用select前设置描述符屏蔽位,在调用 select后使用FD_ISSET来检测文件描述符集fdset中对应于文件描述符fd的位是否被设置。
过去,描述符集被作为一个整数位屏蔽码得到实现,但是这种实现对于多于32个的文件描述符将无法工作。描述符集现在通常用整数数组中的位域表示,数组元素的每一位对应一个文件描述符。例如,一个整数占32位,那么整数数组的第一个元素代表文件描述符0到31,数组的第二个元素代表文件描述符32到63,以此类推。宏FD_SET设置整数数组中对应于fd文件描述符的位为1,宏FD_CLR设置整数数组中对应于fd文件描述符的位为0,宏FD_ZERO设置整数数组中的所有位都为0。假设执行如下程序后:
#include
#include
fd_set readset;
FD_ZERO(&readset);
FD_SET(5, &readset);
FD_SET(33, &readset);
再执行如下程序后:
FD_CLR(5, &readset);
通常,操作系统通过宏FD_SETSIZE来声明在一个进程中select所能操作的文件描述符的最大数目。例如:
在4.4BSD的头文件中我们可以看到:
#ifndef FD_SETSIZE
#define FD_SETSIZE 1024
#endif
在红帽Linux的头文件
#define __FD_SETSIZE 1024
以及在头文件
#include
#define FD_SETSIZE __FD_SETSIZE
既定义FD_SETSIZE为1024,一个整数占4个字节,既32位,那么就是用包含32个元素的整数数组来表示文件描述符集。我们可以在头文件中修改这个值来改变select使用的文件描述符集的大小,但是必须重新编译内核才能使修改后的值有效。当前版本的unix操作系统没有限制FD_SETSIZE 的最大值,通常只受内存以及系统管理上的限制。
我们明白了文件描述符集的实现机制之后,就可对其进行灵活运用。(以下程序在红帽Linux 6.0下运行通过,函数fd_isempty用于判断文件描述符集是否为空;函数fd_fetch取出文件描述符集中的所有文件描述符)
#include
#include
#include
#include
struct my_fd_set{
fd_set fs; //定义文件描述符集fs
unsigned int nconnect; //文件描述符集fs中文件描述符的个数
unsigned int nmaxfd; //文件描述符集fs中最大的文件描述符
};
/* 函数fd_isempty用于判断文件描述符集是否为空,为空返回1,不为空则返回0 */
int fd_isempty(struct my_fd_set *pfs)
{
int i;
/* 文件描述符集fd_set是通过整数数组来实现的,所以定义整数数组myset的元素个数为文件描述符集fd_set所占内存空间的字节数除以整数所占内存空间的字节数。
*/
unsigned int myset[sizeof(fd_set) / sizeof(int)];
/* 把文件描述符集pfs->fs 拷贝到数组myset */
memcpy(myset, &pfs->fs, sizeof(fd_set));
for(i = 0; i < sizeof(fd_set) / sizeof(int); i++)
/* 如果myset的某个元素不为0,说明文件描述符集不为空,则函数返回0 */
if (myset[i])
return 0;
return 1; /* 如果myset的所有元素都为0,说明文件描述符集为空,则函数返回1 */
}
/* 函数fd_fetch对文件描述符集进行位操作,把为1的位换算成相应的文件描述符,然后就可对其进行I/O操作 */
void fd_fetch(struct my_fd_set *pfs)
{
struct my_fd_set *tempset; //定义一个临时的结构指针
unsigned int myset[sizeof(fd_set)/sizeof(unsigned int)];
unsigned int i, nbit, nfind, ntemp;
tempset = pfs;
memcpy(myset, &tempset->fs, sizeof(fd_set));
/* 把最大的文件描述符maxfd除以整数所占的位数,得出maxfd在文件描述符集中相应的位对应于整数数组myset的相应元素的下标,目的是为了减少检索的次数 */
nfind = tempset->nmaxfd / (sizeof(int)*8);
for (i = 0; i <= nfind; i++) {
/* 如果数组myset的某个元素为0,说明这个元素所对应的文件描述符集的32位全为0,则继续判断下一元素。*/
if (myset[i] == 0)
continue;
/* 如果数组myset的某个元素不为0,说明这个元素所对应的文件描述符集的32位中有为1的,把myset[i]赋值给临时变量ntemp,对ntemp进行位运算,把为1的位换算成相应的文件描述符 */
ntemp = myset[i];
/* nbit记录整数的二进制位数,对ntemp从低到高位进行&1运算,直到整数的最高位,或直到文件描述符集中文件描述符的个数等于0 */
for (nbit = 0; tempset->nconnect && (nbit < sizeof(int)*8); nbit++) {
if (ntemp & 1) {
/* 如果某位为1,则可得到对应的文件描述符为nbit + 32*I,然后我们可对其进行I/O操作。这里我只是做了简单的显示。*/
printf("i = %d, nbit = %d, The file description is %d\n", i, nbit, nbit + 32*i);
/* 取出一个文件描述符后,将文件描述符集中文件描述符的个数减1 */
tempset->nconnect--; }
ntemp >>= 1; // ntemp右移一位
}
}
}
/* 下面的主程序是对以上两个函数的测试 */
main()
{
/* 假设fd1,fd2,fd3为3个文件描述符,实际运用中可为Socket描述符等 */
int fd1 = 7, fd2 = 256, fd3 = 1023, isempty;
struct my_fd_set connect_set;
connect_set.nconnect = 0;
connect_set.nmaxfd = 0;
FD_ZERO(&connect_set.fs);
/* FD_SET操作前对函数fd_isempty进行测试 */
isempty = fd_isempty(&connect_set);
printf("isempty = %d\n", isempty);
FD_SET(fd1, &connect_set.fs);
FD_SET(fd2, &connect_set.fs);
FD_SET(fd3, &connect_set.fs);
connect_set.nconnect = 3;
connect_set.nmaxfd = fd3 ;
/* FD_SET操作后,既把文件描述符加入到文件描述符集之后,对函数fd_isempty进行测试 */
isempty = fd_isempty(&connect_set);
printf("isempty = %d\n", isempty);
/* 对函数fd_ fetch进行测试 */
fd_fetch(&connect_set);
}
/* 程序输出结果为 :*/
isempty is 1
isempty is 0
i = 0, nbit = 7, The file description is 7
i = 8, nbit = 0, The file description is 256
i = 31, nbit = 31, The file description is 1023