在文章 <<linux socke编程实例:一个简单的echo服务器程序>>简单地介绍了如何在linix使用socket进行网络编程,并且在文中给出相应的程序和说明,以使大家对socket编程有初步的了解。现在我们解决文中出现的问题:不能同时与多个客户进行通信,只能第一个客户通信结束事才能与第二个客户进行通信,依次...直到有的客户连接超时,或者没有客户进行连接。通常一个服务器程序应能同时与多个客户端进行通信,能及时对它的请求作出回复。
其实这个问题有很多解方案,最明显的一种就是采用多线程或者采用多进程。显然多进程是不可取的,因此有多线程的存。采用多线程技术时,每当有客户请求连接,服务器都是新建一个线程与之通信,通信完成后将终止该线程。这样的情况适合于大量IO操作的服务器程程,或者服务器的是多核的。否则,这样未免太浪费了,系统的开销也会很大。因此我们致力于采用单进程单线的解决方案。
其实,我们对C/S通信方式中的服务器端会有这样的直觉:服务器等待客户的到来,如果在一定的时间内(10ms),如果没有收到请求,那就不等了,马上回来处量已连接上的客户端,并与之通信。与已连接上的客户端进行通信,也采用这样的方式,对于任一客户端,看它有没有数据到来,有则处理,没则轮到下一个客户,采用相同的处理方式。依次进行下去,就不需要使用多进程或多线程技术,可以如何实现呢?
其实IO可以分为阻塞IO和非阻塞IO,当读阻塞IO的数据时,直到有数据时才返回,否则一直阻塞;非阻塞IO却不同,读非阻塞IO的数据时,不管有没有数据到来,它都会及时返回,有 没 有数据到来依据返回值进行判断。无论我们在代码中使用open还是socket函数返回的文件描述符,它都是阻塞的,要使它变为非阻塞,则必须使用fcntl函数对它的属性进行更改。fcntl函数说明如下:
int
fcntl(
int
fd
int
cmd,...
/*
int arg * / ) ;
fd为文件描述符,cmd表示命令,可变参数的个数和类型根据cmd值的不同而不同。
f c n t l函数有五种功能:
复制一个现存的描述符(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)。
要设置文件的非阻塞属生,先要用cmd=F_GETFD获得属性值,然后把属性值为非阻塞的即可。
为了方便设置文件的属生,我们定义了如下的函数:
void
set_fl(
int
fd,
int
flags)
...
{
int val;
//获取属性
if((val = fcntl(fd, F_GETFL, 0)) < 0)
...{
printf("get socket property error ");
exit(0);
}
//更改属性
val = val |flags;
//更改属性后再设置,以使它使效
if(fcntl(fd, F_SETFL, val) < 0)
...{
printf("set socket property error ");
exit(1);
}
}
在函数set_fl中,可以对文件fd设置属性flags,如果我们要把描述符socketfd设置为非阻塞,只需如下语句即可:
set_fl(socketfd, O_NONBLOCK);
下面是采用非阻塞IO机制来实现的代码,与原来的相比,结构和原理上没有改变,只是把所有的IO都设置为非阻塞的。
#include
<
netdb.h
>
#include
<
sys
/
socket.h
>
#include
<
errno.h
>
#include
<
stdio.h
>
#include
<
unistd.h
>
#include
<
fcntl.h
>
#define
EHCO_PORT 8080
#define
MAX_CLIENT_NUM 100
void
set_fl(
int
fd,
int
flags)
{
int
val;
if
((val
=
fcntl(fd, F_GETFL,
0
))
<
0
)
{
printf(
"
get socket property error
"
);
exit(
0
);
}
val
=
val
|
flags;
if
(fcntl(fd, F_SETFL, val)
<
0
)
{
printf(
"
set socket property error
"
);
exit(
1
);
}
}
int
main()
{
int
socketfd;
socketfd
=
socket(AF_INET, SOCK_STREAM,
0
);
if
(socketfd
==
-
1
)
{
printf(
"
errno=%d
"
, errno);
exit(
1
);
}
else
{
printf(
"
socket create successfully
"
);
}
struct
sockaddr_in sa;
bzero(
&
sa,
sizeof
(sa));
sa.sin_family
=
AF_INET;
sa.sin_port
=
htons(EHCO_PORT);
sa.sin_addr.s_addr
=
htons(INADDR_ANY);
bzero(
&
(sa.sin_zero),
8
);
if
(bind(socketfd, (
struct
sockaddr
*
)
&
sa,
sizeof
(sa))
!=
0
)
{
printf(
"
bind failed
"
);
printf(
"
errno=%d
"
, errno);
exit(
1
);
}
else
{
printf(
"
bind successfully
"
);
}
//
listen
if
(listen(socketfd ,MAX_CLIENT_NUM)
!=
0
)
{
printf(
"
listen error
"
);
exit(
1
);
}
else
{
printf(
"
listen successfully
"
);
}
int
clientfd;
struct
sockaddr_in clientAdd;
socklen_t len
=
sizeof
(clientAdd);
char
buff[
100
];
int
closing
=
0
;
int
clientArray[MAX_CLIENT_NUM];
int
i;
for
(i
=
0
; i
<
MAX_CLIENT_NUM;
++
i)
{
clientArray[i]
=
-
1
;
//
-1 indicates no using
}
// set no blocking for server, and the accept function will not block.
set_fl(socketfd, O_NONBLOCK);
while
( closing
==
0
)
{
clientfd
=
accept(socketfd, (
struct
sockaddr
*
)
&
clientAdd,
&
len);
if
(clientfd
>
0
)
{
printf(
"
receive connection
"
);
//set no blocking for client, in that the read function will no block.
set_fl(clientfd, O_NONBLOCK);
for
(i
=
0
; i
<
MAX_CLIENT_NUM;
++
i)
{
if
(clientArray[i]
==
-
1
)
{
clientArray[i]
=
clientfd;
break
;
}
}
if
(i
==
MAX_CLIENT_NUM)
{
//
there is not enough sockets for client, so close it
close(clientfd);
}
}
int
n;
for
(i
=
0
; i
<
MAX_CLIENT_NUM;
++
i)
{
if
(clientArray[i]
==
-
1
)
continue
;
if
( (n
=
recv(clientArray[i], buff,
100
,
0
))
>
0
)
{
printf(
"
number of receive bytes = %d
"
, n);
write(STDOUT_FILENO, buff, n);
send(clientArray[i],buff, n,
0
);
buff[n]
=
0
;
if
(strcmp(buff,
"
quit/r/n
"
)
==
0
)
{
close(clientArray[i]);
clientArray[i]
=
-
1
;
break
;
}
else
if
(strcmp(buff,
"
close
/r/n"
)
==
0
)
{
//
server closing
closing
=
1
;
printf(
"
server is closing
"
);
break
;
}
}
}
//
printf("receive finsihed ");
}
for
(i
=
0
; i
<
MAX_CLIENT_NUM;
++
i)
{
if
(clientArray[i]
!=
-
1
)
{
close(clientArray[i]);
}
}
close(socketfd);
return
0
;
}
值得一提的是,当文件为非阻塞时,accept会回返回它接受连接的客户端描述符,如没有客户请求连接,则返回-1;同理,recv会返回该客户端发送过来的数据的字节数,如没有数据到来,则返回-1。因此我们可以根据这两个函数的返回值进行相应的操作。accept返回非负数的情况,说明有新的客户请求连接,因此要把它记录下来,以下次能收到它发送过来的信息。对于recv也一样,如果返回值为正数,则说明有数所到来,对数据作相应的处理返回复。
经此一役,使用单进程的通信方式仍然可以实现服务器程序。可以使用上文中介绍的方法(telnet)对它进行测试。
在测试的过程中,我们依然可以发现:服务器采用了非阻塞的方式进行读,当没有数据准备好的时候,它不停地循环,这样浪费了大量的CPU时间。因此,本文的方案仍不是一个最好的方案。
如果客户端的通信量很少,那么服务器会不停地循环,这样就会占用很多CPU时间,使得其它进程使用的时间很少。
如果解决这个问题呢?在下一篇章和大家一起探讨一下解决的方法。