Chinaunix首页 | 论坛 | 博客
  • 博客访问: 174511
  • 博文数量: 31
  • 博客积分: 728
  • 博客等级: 军士长
  • 技术积分: 295
  • 用 户 组: 普通用户
  • 注册时间: 2012-03-28 15:29
个人简介

To be Ols!

文章分类

全部博文(31)

文章存档

2013年(1)

2012年(30)

我的朋友

分类:

2012-10-12 21:31:41

原文地址:串口编程 作者:futter521


串口编程

http://blog.chinaunix.net/u1/52454/showart_507112.html

在嵌入式Linux中,串口是一个字设备,访问具体的串行端口的编程与读/写文件的操作类似,只需打开相应的设备文件即可操作。串口编程特殊在于串口通信时相关参数与属性的设置。嵌入式Linux的串口编程时应注意,若在根文件中没有串口设备文件,应使用mknod命令创建,这这里假设串口设备是/dev/ttyS0,介绍一下串口的编程过程。
mknod /dev/ttyS0 c 4 64
1
、打开串口
      
打开串口设备文件的操作与普通文件的操作类似,都采用标准的I/O操作函数open()
       fd = open("/dev/ttyS0",O_RDWR|O_NDELAY|O_NOCTTY);
    open()
函数有两个参数,第一个参数是要打开的文件名(此处为串口设备文件/dev/ttyS0);第二个参数设置打开的方式,O_RDWR表示打开的文件可读/写,O_NDELAY表示以非阻塞方式打开,O_NOCTTY表示若打开的文件为终端设备,则不会将终端作为进程控制终端。
2
、设置串口属性
    
串口通信时的属性设置是串口编程的关键问题,许多串口通信时的错误都与串口的设置相关,所以编程时应特别注意这些设置,最常见的设置包括波特率、奇偶校验和停止位以及流控制等。
   
Linux中,串口被作为终端I/O,它的参数设置需要使用struct termios结构体,这个结构体在termio.h文件中定义,且应在程序中包含这个头文件。
          typedef unsigned char         cc_t ;
          typedef unsigned int            speed_t ;
          typedef unsigned int            tcflag_t ;
          struct termios
          {
             tcflag_t      c_iflag ;          /*
输入模式标志*/
             tcflag_t      c_oflag ;         /*
输出模式标志*/
             tcflag_t      c_cflag ;            /*
控制模式标志*/
             tcflag_t      c_lflag ;            /*
本地模式标志*/
             tcflag_t      c_line ;            /*
行规程类型,一般应用程序不使用*/
             cc_t         c_cc[NCC];      /*
控制字符*/
             speed_t      c_ispeed ;      /*
输入数据波特率*/
             speed_t      c_ospeed ;      /*
输出数据波特率*/
       };
      
串口的设置主要是设置这个结构体的各成员值,然后利用该结构体将参数传给硬件驱动程序。在Linux中,串口以串行终端的方式进行处理,因而,可以使用tcgetattr()/tcsetattr()函数获取/设置串口的参数。
       int tcgetattr( int fd, struct termios *termios_p );
       int tcsetattr( int fd, int optional_actions , struct termios *termios_p );
      
这两个参数都有一个指向termios结构体的指针作为参数,用于返回当前终端的属性或设置该终端的属性。参数fd就是用open()函数打开的终端文件句柄,而串口就是用open()打开的串口设备文件句柄。tcsetattr()函数的optional_action参数用于指定新设定的参数起作用的时间,其设定值可以为:
       TCSANOW        
改变立即生效
       TCSADRAIN     
在所有的输出都被传输后改变生效,适用于更改影响输出参数的情况。
       TCSAFLUSH   
在所有输出都被传输后改变生效,丢弃所有末读入的输入(清空输入缓                                存)。
1)设置波特率
      
使用cfsetospeed()/cfsetispeed()函数设置波特率,它们分别用于在termios结构体中设置输出和输入的波特率。设置波特率可以使用波特率常数,其定义为字母“B+速率,如B19200就是波特率为19200bpsB115200就是波特率为115200bps
int cfsetispeed( struct termios *termios_p, speed_t speed );     //speed
为波特率常数
int cfsetospeed( struct termios *termios_p, speed_t speed );

cfsetispeed( ttys0_opt, B115200 );
cfsetospeed( ttys0_opt, B115200 );
2)设置控制模式标志
      
控制模式标志c_cflag主要用于设置串口对DCD信号状态检测、硬件流控制、字符位宽、停止位和奇偶校验等,常用标志位如下:
CLOCAL  
忽略DCD信号,若不使用MODEM,或没有串口没有CD脚就设置此标志
CREAD   
启用接收装置,可以接收字符
CRTSCTS
启用硬件流控制,对于许多三线制的串不应使用,需设置~CRTCTS
CSIZE     
字符位数掩码,常用CS8
CSTOPB  
使用两个停止位,若用一位应设置~CSTOPB
PARENB  
启用奇偶校验
      
例如,下面的代码将串口设置为忽略DCD信号,启用接收装置,关闭硬件流控制,传输数据时使用8位数据位和一位停止位(8N1),不使用奇偶校验。
       struct temios ttys0
       ttyso_opt.c_cflag |= CLOCAL | CREAD ;      //
CLOCALCREAD位设置为1
       ttys0_opt.c_cflag &= ~CRTSCTS ;               //
将硬件流控制位CRTSCTS0,其他位不变
       ttys0_opt.c_cflag &
CSIZE                //清除数据位掩码
       ttys0_opt.c_cflag |= CS8 ;                           //
设置8位数据位标志CS8
       ttys0_opt.c_cflag &= ~(PARENB|CSTOPB);//
使用1位停止位,停用奇偶校验
3)设置本地模式标志
      
本地模式标志c_lflag主要用于设置终端与用户的交互方式,常见的设置标志位有ICANONECHOECHOE等。其中,ICANON标志位用于实现规范输入,即read()读到行结束符后返回,常用于终端的处理;若串口用于发送/接收数据,则应清除此标志,使用非规范模式(raw mode)。非规范模式中,输入数据不组成行,不处规范模式中的特殊字符。在规范模式中,当设置ECHO标志位时,用户向终端输入的字符将被回传给用户;当设置ECHOE标志位时,用户输入退格键时,则回传退格-空格-退格序列给用户,使得退格键覆盖的字符从显示中消失,这样更符合用户的习惯(若未设置此标志,输入退格键时,则光标回退一个字符,但原有的字符未从显示中消失)。
4)设置输入模式标志
      
输入模式标志c_iflag主要用于控制串口的输入特性,常用的设置有IXOFFIXON,分别用于软件流控制。其中,IXOFF用于防止输入缓冲区溢出;IXON则是在输入数据中识别软件流控制标志。由于许多嵌入式系统无法使用硬件流控制,因此,只能使用软件流控制数据传输的速度,但是,它可能降低串口数据传输效率。启用软件流控制的代码如下:
       ttys0_opt.c_iflag |= IXOFF|IXON ;
5)设置输出模式标志
      
输出模式标志c_oflag主要用于对串口在规范模式时输出的特殊字符处理,而对非规范模式无效。
6)设置控制字符
      
在非规范模式中,控制字符数组c_cc[]中的变量c_cc[VMIN]c_cc[VTIME]用于设置read()返回前读到的最少字节数和读超时时间,其值分为四种情况:
    (a)c_cc[VMIN]
0c_cc[VTIME]>0
         
读到一个字节后,启动定时器,其超时时间为c_cc[VTIME],read()返回的条件为至少读到c_cc[VMIN]个字符或定时器超期。
    (b)c_cc[VMIN]>0, c_cc[VTIME] ==0
         
只要读到数据的字节数大于等于c_cc[VMIN],则read()返回;否则,将无限期阻塞等待。
      (c)c_cc[VMIN] == 0, c_cc[VTIME]>0
         
只要读到数据,则read()返回;若定时器超期(定时时间c_cc[VTIME])却未读到数据,则read()返回0
      (d)c_cc[VMIN] == 0, c_cc[VTIME] == 0
         
若有数据,则read()读取指定数量的数据后返回;若没有数据,则read()返回0
termios结构体中填写完这些参数后,接下来就可以使用tcsetattr()函数设置串口的属性。
       tcsetattr( fd, &old_opt );         //
将原有的设置保存到old_opt,以便程序结束后恢复
       tcsetattr( fd, TCSANOW, &ttsy0_opt );
3
、清空发送/接收缓冲区
      
为保证读/写操作不被串口缓冲区中原有的数据干拢,可以在读/写数据前用tcflush()函数清空串口发送/接收缓冲区。tcflush()函数的参数可为:
        TCIFLUSH     
清空输入队列
        TCOFLUSH  
清空输出队列
        TCIOFLUSH  
同时清空输入和输出队列
4
、从串口读写数据
       
串口的数据读/写与普通文件的读/写一样,都是使用read()/write()函数实现。
        n = write( fd, buf, len );         //
buflen个字节的数据从串口输出,返回输出的字节数
        n = read( fd, buf, len );         //
从串口读入len个字节的数据并放入buf, 返回读取的字节数
5
、关闭串口
   
关闭串口的操作很简单,将打开的串口设备文件句柄关闭即可。
    close(fd);

 

Linux 串口读写()

例子

下面是一个简单的读取串口数据的例子,使用了上面定义的一些函数和头文件

/**********************************************************************

 * 代码说明:使用串口二测试的,发送的数据是字符,但是没有发送字符串结束符号,

 * 所以接收到后,后面加上了结束符号。我测试使用的是单片机发送数据到第二个串口,测试通过。

 **********************************************************************/

#define FALSE  -1

#define TRUE   0

/*********************************************************************/

int OpenDev(char *Dev)

{

    //Dev 就是设备,设备就是文件,就是给出该设备文件的路径

    int fd = open(Dev, O_RDWR ); //| O_NOCTTY | O_NDELAY

    if (-1 == fd)

    {

       perror("Can't Open Serial Port");

       return -1;

    }

    else

       return fd;

}

int main(int argc, char **argv)

{

    int fd;

    int nread;

    char buff[512];

    char *dev = "/dev/ttyS1"; //串口二

    fd = OpenDev(dev);

    set_speed(fd, 19200);

    if (set_Parity(fd, 8, 1, 'N') == FALSE)

    {

       printf("Set Parity Error\n");

       exit (0);

    }

    while (1) //循环读取数据

    {

       while ((nread = read(fd, buff, 512))>0)

       {

           printf("\nLen %d\n", nread);

           buff[nread+1] = '\0';

           printf("\n%s", buff);

       }

    }

    //close(fd); 

    // exit (0);

}

 

1、虚拟机下使用串口的方法
      使用vmwave,默认串口设备是没有添加的,通过vmwave将设备加入即可正常使用串口。虚拟机串口打开后,可能会占用windows下的串口。另外,虚拟机的串口收发比正常的速度的确要慢许多。

 

2、消除Linux串口收发的一些规则

Linux 串口收发有许多模式,如:

1 接收返回模式: 如果串口没有接收到数据,read()函数不返回。

2 数据接收\n才返回接收的数据,否则read()函数返回0

3 特殊字符解析问题,部分特殊字符接收/发送时,会被屏蔽或者转义。如发送0x0A 接收变为0x0A 0x0A 0x0D被屏蔽等。

4 接收反馈:如串口接收到数据,立即将该数据发送出去。

(上面是我遇到的一些问题,可能表述不很清楚,呵呵。如果用于收发txt文件,一般不大注意。)

 

3、解决问题的方法是,消除这些默认规则,关键是struct termios 的参数影响。

struct termios  {

       tcflag_t c_iflag;               /**//* 输入模式旗标 */

        tcflag_t c_oflag;               /**//* 输出模式旗标 */

        tcflag_t c_cflag;               /**//* 控制模式旗标 */

        tcflag_t c_lflag;               /**//* 区域模式旗标 */

        cc_t c_line;                    /**//* 行控制 (line discipline) */

        cc_t c_cc[NCCS];           /**//* 控制特性 */

};

 

 

由于研究不深,如果要消除所有上面的规则,我是如下处理的

struct termios options;

 串口打开方式:

  open ("dev/ttyS0" , O_RDWR|O_NOCTTY| O_NDELAY );

 消除收发模式规则:

options.c_lflag        = 0;

options.c_oflag        = 0;

options.c_iflag        = 0;

 

消除字符屏蔽规则:

options.c_cc[VINTR]    = 0;       /**//* Ctrl-c */

options.c_cc[VQUIT]     = 0;   /**//* Ctrl- */

options.c_cc[VERASE]    = 0;   /**//* del */

options.c_cc[VKILL]    = 0;   /**//* @ */

options.c_cc[VEOF]     = 0;   /**//* Ctrl-d */

options.c_cc[VTIME]    = 1;   /**//*  */

options.c_cc[VMIN]     = 0;   /**//*  */

options.c_cc[VSWTC]    = 0;   /**//* '' */

options.c_cc[VSTART]   = 0;   /**//* Ctrl-q */

options.c_cc[VSTOP]    = 0;   /**//* Ctrl-s */

options.c_cc[VSUSP]    = 0;   /**//* Ctrl-z */

options.c_cc[VEOL]     = 0;   /**//* '' */

options.c_cc[VREPRINT] = 0;   /**//* Ctrl-r */

options.c_cc[VDISCARD] = 0;   /**//* Ctrl-u */

options.c_cc[VWERASE]  = 0;   /**//* Ctrl-w */

options.c_cc[VLNEXT]   = 0;   /**//* Ctrl-v */

options.c_cc[VEOL2]    = 0;   /**//* '' */

 

以上设置,在其它参数串口设置前执行,如果你需要保留部分参数,请参阅http://blog.chinaunix.net/article.php?articleId=15964&blogId=60

RedHat Feroda 4 下编译通过

 

= = = = = = = = = = = 非阻塞read= = = = = = = = = = =

Q:在调用串口read(fd,   buff,   len);,如果串口没有数据,会停在read,请问有没有办法让这个read动作中止?

A:使用非阻塞方式select函数(I/O多工机制)或者open的时候加O_NONBLOCK参数。

int select(int n,fd_set * readfds,fd_set * writefds,fd_set * exceptfds,struct timeval * timeout);关于这个函数的使用我会在下篇blog中整理。

= = = = = = = = = = = 串口收发源码= = = = = = = = = = =

       一下代码已经经过我测试,没有问题。开发环境Redhat9,运行环境s3c2410

= = = = = = receive.c= = = = = =

#include    

#include    

#include        

#include    

#include     

#include       

#include     

#include       

#include  

 

#define TRUE 1

//初始化串口选项:  

void setTermios(struct termios * pNewtio, int uBaudRate)

{

    bzero(pNewtio, sizeof(struct termios)); /* clear struct for new port settings */

 

    //8N1

    pNewtio->c_cflag = uBaudRate | CS8 | CREAD | CLOCAL;

    pNewtio->c_iflag = IGNPAR;

 

    pNewtio->c_oflag = 0;

    pNewtio->c_lflag = 0; //non ICANON

    /*

     initialize all control characters

     default values can be found in /usr/include/termios.h, and

     are given in the comments, but we don't need them here

     */

    pNewtio->c_cc[VINTR] = 0; /* Ctrl-c */

    pNewtio->c_cc[VQUIT] = 0; /* Ctrl-\ */

    pNewtio->c_cc[VERASE] = 0; /* del */

    pNewtio->c_cc[VKILL] = 0; /* @ */

    pNewtio->c_cc[VEOF] = 4; /* Ctrl-d */

    pNewtio->c_cc[VTIME] = 5; /* inter-character timer, timeout VTIME*0.1 */

    pNewtio->c_cc[VMIN] = 0; /* blocking read until VMIN character arrives */

    pNewtio->c_cc[VSWTC] = 0; /* '\0' */

    pNewtio->c_cc[VSTART] = 0; /* Ctrl-q */

    pNewtio->c_cc[VSTOP] = 0; /* Ctrl-s */

    pNewtio->c_cc[VSUSP] = 0; /* Ctrl-z */

    pNewtio->c_cc[VEOL] = 0; /* '\0' */

    pNewtio->c_cc[VREPRINT] = 0; /* Ctrl-r */

    pNewtio->c_cc[VDISCARD] = 0; /* Ctrl-u */

    pNewtio->c_cc[VWERASE] = 0; /* Ctrl-w */

    pNewtio->c_cc[VLNEXT] = 0; /* Ctrl-v */

    pNewtio->c_cc[VEOL2] = 0; /* '\0' */

}

 

#define BUFSIZE 512

int main(int argc, char **argv)

{

    int fd;

    int nread;

    char buff[BUFSIZE];

    struct termios oldtio, newtio;

    struct timeval tv;

    char *dev ="/dev/ttyS1";

    fd_set rfds;

 

    if ((fd = open(dev, O_RDWR | O_NOCTTY))<0)

    {

       printf("err: can't open serial port!\n");

       return -1;

    }

 

    tcgetattr(fd, &oldtio); /* save current serial port settings */

    setTermios(&newtio, B115200);

 

    tcflush(fd, TCIFLUSH);

    tcsetattr(fd, TCSANOW, &newtio);

 

    tv.tv_sec=30;

    tv.tv_usec=0;

    while (TRUE)

    {

       printf("wait...\n");

       FD_ZERO(&rfds);

       FD_SET(fd, &rfds);

       if (select(1+fd, &rfds, NULL, NULL, &tv)>0)

       {

           if (FD_ISSET(fd, &rfds))

           {

              nread=read(fd, buff, BUFSIZE);

              printf("readlength=%d\n", nread);

              buff[nread]='\0';

              printf("%s\n", buff);

           }

       }

    }

    tcsetattr(fd, TCSANOW, &oldtio);

    close(fd);

}

 

= = = = = send.c= = = = = =

#include        

#include      

#include        

#include    

#include     

#include       

#include     

#include       

#include  

 

//初始化串口选项:  

void setTermios(struct termios * pNewtio, int uBaudRate)

{

    bzero(pNewtio, sizeof(struct termios)); /* clear struct for new port settings */

 

    //8N1

    pNewtio->c_cflag = uBaudRate | CS8 | CREAD | CLOCAL;

    pNewtio->c_iflag = IGNPAR;

 

    pNewtio->c_oflag = 0;

    pNewtio->c_lflag = 0; //non ICANON

    /*

     initialize all control characters

     default values can be found in /usr/include/termios.h, and

     are given in the comments, but we don't need them here

     */

    pNewtio->c_cc[VINTR] = 0; /* Ctrl-c */

    pNewtio->c_cc[VQUIT] = 0; /* Ctrl-\ */

    pNewtio->c_cc[VERASE] = 0; /* del */

    pNewtio->c_cc[VKILL] = 0; /* @ */

    pNewtio->c_cc[VEOF] = 4; /* Ctrl-d */

    pNewtio->c_cc[VTIME] = 5; /* inter-character timer, timeout VTIME*0.1 */

    pNewtio->c_cc[VMIN] = 0; /* blocking read until VMIN character arrives */

    pNewtio->c_cc[VSWTC] = 0; /* '\0' */

    pNewtio->c_cc[VSTART] = 0; /* Ctrl-q */

    pNewtio->c_cc[VSTOP] = 0; /* Ctrl-s */

    pNewtio->c_cc[VSUSP] = 0; /* Ctrl-z */

    pNewtio->c_cc[VEOL] = 0; /* '\0' */

    pNewtio->c_cc[VREPRINT] = 0; /* Ctrl-r */

    pNewtio->c_cc[VDISCARD] = 0; /* Ctrl-u */

    pNewtio->c_cc[VWERASE] = 0; /* Ctrl-w */

    pNewtio->c_cc[VLNEXT] = 0; /* Ctrl-v */

    pNewtio->c_cc[VEOL2] = 0; /* '\0' */

}

 

int main(int argc, char **argv)

{

    int fd;

    int nCount, nTotal, i;

    struct termios oldtio, newtio;

    char *dev ="/dev/ttyS1";

 

    if ((argc!=3) || (sscanf(argv[1], "%d", &nTotal) != 1))

    {

       printf("err: need tow arg!\n");

       return -1;

    }

 

    if ((fd = open(dev, O_RDWR | O_NOCTTY))<0)

    {

       printf("err: can't open serial port!\n");

       return -1;

    }

 

    tcgetattr(fd, &oldtio); /* save current serial port settings */

    setTermios(&newtio, B115200);

 

    tcflush(fd, TCIFLUSH);

    tcsetattr(fd, TCSANOW, &newtio);

 

    for (i=0; i

    {

       nCount=write(fd, argv[2], strlen(argv[2]));

       printf("send data\n");

       sleep(1);

    }

    tcsetattr(fd, TCSANOW, &oldtio);

    close(fd);

    return 0;

}

 

= = = = = =.makefile= = = = = =

CC = /usr/local/arm/2.95.3/bin/arm-linux-gcc

all:receive send

receive: receive.c

    $(CC) receive.c -o  receive

send: send.c

    $(CC) send.c -o  send

clean:

    -rm -rf testCOM receive send

 

到此基本就结束了,可能代码注释比较少些,写的太着急了,等有时间整理一下。最好再看看上一篇blog这样能更好的理解串口。

 

#include

  #include

  #include

#define STDIN 0 /* file descriptor for standard input */

main()

   {

  struct timeval tv;

  fd_set readfds;

tv.tv_sec = 2;

  tv.tv_usec = 500000;

FD_ZERO(&readfds);

  FD_SET(STDIN,&readfds);

/* don&apost care about writefds and exceptfds: */

  select(STDIN+1,&readfds, NULL, NULL,&tv);

if (FD_ISSET(STDIN,&readfds))

  printf("A key was pressed!\n");

  else

  printf("Timed out.\n");

  }

如果你是在一个 line buffered 终端上,那么你敲的键应该是回车 (RETURN),否则无论如何它都会超时。

现在,你可能回认为这就是在数据报套接字上等待数据的方式--你是对 的:它可能是。有些 Unix 系统可以按这种方式,而另外一些则不能。你 在尝试以前可能要先看看本系统的 man page 了。

最后一件关于 select() 的事情:如果你有一个正在侦听 (listen()) 的套 接字,你可以通过将该套接字的文件描述符加入到 readfds 集合中来看是 否有新的连接。

这就是我关于函数select() 要讲的所有的东西。

 

 

深入UNIX编程:一个简单聊天室的两种实现 (fcntl select)

 

--------------------------------------------------------------------------------

 

 

2004-12-3 互联网

 

        

      在互联网相当普及的今天,在互联网上聊天对很多“网虫”来说已经是家常便饭了。聊天室程序可以说是网上最简单的多点通信程序。聊天室的实现方法有很多,但都是利用所谓的“多用户空间”来对信息进行交换,具有典型的多路I/O

架构。一个简单的聊天室, 从程序员的观点来看就是在多个I/O端点之间实现多对多的通信。其架构如图一所示。这样的实现在用户的眼里就是聊天室内任何一个人输入一段字符之后,其他用户都可以得到这一句话。这种“多用户空间”的架构在其他多点通信程序中应用的非常广泛,其核心就是多路I/O通信。多路I/O通信又被称为I/O多路复用(I/O Multiplexing)一般被使用在以下的场合:

 

      客户程序需要同时处理交互式的输入和同服务器之间的网络连接时需要处理I/O多路复用问题;

      客户端需要同时对多个网络连接作出反应(这种情况很少见);

      TCP服务器需要同时处理处于监听状态和多个连接状态的socket

      服务器需要处理多个网络协议的socket;

      服务器需要同时处理不同的网络服务和协议。

 

      聊天室所需要面对的情况正是第一和第三两种情况。我们将通过在TCP/IP协议之上建立一个功能简单的聊天室让大家更加了解多路I/O以及它的实现方法。 我们要讨论的聊天室功能非常简单, 感兴趣的朋友可以将其功能扩展, 发展成一个功能比较完整的聊天室, 如加上用户认证, 用户昵称, 秘密信息, semote 等功能. 首先它是一个 client/server 结构的程序, 首先启动 server, 然后用户使用 client 进行连接. client/server 结构的优点是速度快, 缺点是当 server 进行更新时, client 也必需更新.

 

网络初始化

 

      首先是初始化 server, 使server 进入监听状态: (为了简洁起见,以下引用的程序与实际程序略有出入, 下同)

sockfd = socket( AF_INET,SOCK_STREAM, 0);

// 首先建立一个 socket, 族为 AF_INET, 类型为 SOCK_STREAM.

// AF_INET = ARPA Internet protocols 即使用 TCP/IP 协议族

// SOCK_STREAM 类型提供了顺序的, 可靠的, 基于字节流的全双工连接.

// 由于该协议族中只有一个协议, 因此第三个参数为 0

 

bind( sockfd, ( struct sockaddr *)&serv_addr, sizeof( serv_addr));

// 再将这个 socket 与某个地址进行绑定.

// serv_addr 包括 sin_family = AF_INET 协议族同 socket

// sin_addr.s_addr = htonl( INADDR_ANY) server 所接受的所有其他

// 地址请求建立的连接.

// sin_port = htons( SERV_TCP_PORT) server 所监听的端口

// 在本程序中, server IP和监听的端口都存放在 config 文件中.

 

listen( sockfd, MAX_CLIENT);

// 地址绑定之后, server 进入监听状态.

// MAX_CLIENT 是可以同时建立连接的 client 总数.

server 进入 listen 状态后, 等待 client 建立连接。

 

Client端要建立连接首先也需要初始化连接:

sockfd = socket( AF_INET,SOCK_STREAM,0));

// 同样的, client 也先建立一个 socket, 其参数与 server 相同.

 

connect( sockfd, ( struct sockaddr *)&serv_addr, sizeof( serv_addr));

// client 使用 connect 建立一个连接.

// serv_addr 中的变量分别设置为:

// sin_family = AF_INET 协议族同 socket

// sin_addr.s_addr = inet_addr( SERV_HOST_ADDR) 地址为 server

// 所在的计算机的地址.

// sin_port = htons( SERV_TCP_PORT) 端口为 server 监听的端口.

 

client 建立新连接的请求被送到Server端时, server 使用 accept 来接受该连接:

accept( sockfd, (struct sockaddr*)&cli_addr,&cli_len);

// 在函数返回时, cli_addr 中保留的是该连接对方的信息

// 包括对方的 IP 地址和对方使用的端口.

// accept 返回一个新的文件描述符.

 

      server 进入 listen 状态之后, 由于已有多个用户在线,所以程序需要同时对这些用户进行操作,并在它们之间实现信息交换。这在实现上称为I/O多路复用技术。多路复用一般有以下几种方法:

 

      非阻塞通信方法:将文件管道通过fcntl()设为非阻塞通信方式,每隔一端时间对他们实行一次轮询,以判断是否可以进行读写操作。这种方式的缺点是费用太高,大部分资源浪费在轮询上。

 

      子进程方法:应用多个子进程,每一个对一个单工阻塞方式通信。所有子进程通过IPC和父进程进行通信。父进程掌管所有信息。这种方式的缺点是实现复杂,而且由于IPC在各个操作系统平台上并不完全一致,会导致可移植性降低。

 

      信号驱动(SIGIO)的异步I/O方法:首先,异步I/O是基于信号机制的,并不可靠。其次单一的信号不足以提供更多的信息来源。还是需要辅助以其他的手段,实现上有很高的难度。

 

      select ()方法:在BSD中提供了一种可以对多路I/O进行阻塞式查询的方法——select()。它提供同时对多个I/O描述符进行阻塞式查询的方法,利用它,我们可以很方便的实现多路复用。根据统一UNIX规范的协议,POSIX也采用了这种方法,因此,我们可以在大多数操作系统中使用select方法。

 

      使用专门的I/O多路复用器:在“UNIX? SYSTEM V Programmer&aposs Guide: STREAMS”一书中详细的说明了构造和使用多路复用器的方法。这里就不再详述了。

 

我们下面分别讨论多路I/O的两种实现方法:

 

1. 非阻塞通信方法

 

      对一个文件描述符指定的文件或设备, 有两种工作方式: 阻塞与非阻塞。所谓阻塞方式的意思是指, 当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待状态, 直到有东西可读或者可写为止。而对于非阻塞状态, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待。缺省情况下, 文件描述符处于阻塞状态。在实现聊天室时, server 需要轮流查询与各client 建立的 socket, 一旦可读就将该 socket 中的字符读出来并向所有其他client 发送。并且, server 还要随时查看是否有新的 client 试图建立连接,这样, 如果 server 在任何一个地方阻塞了, 其他 client 发送的内容就会受到影响,得不到服务器的及时响应。新 client 试图建立连接也会受到影响。所以我们在这里不能使用缺省的阻塞的文件工作方式,而需要将文件的工作方式变成非阻塞方式。在UNIX下,函数fcntl()可以用来改变文件I/O操作的工作方式,函数描述如下:

 

fcntl( sockfd, F_SETFL, O_NONBLOCK);

// sockfd 是要改变状态的文件描述符.

// F_SETFL 表明要改变文件描述符的状态

// O_NONBLOCK 表示将文件描述符变为非阻塞的.

 

为了节省篇幅我们使用自然语言描述聊天室 server :

while ( 1)

{

      if 有新连接 then 建立并记录该新连接;

      for ( 所有的有效连接)

            begin

                  if 该连接中有字符可读 then

                        begin

                              读入字符串;

                        for ( 所有其他的有效连接)

                              begin

                                    将该字符串发送给该连接;

                              end;

                        end;

            end;

      end.

 

      由于判断是否有新连接, 是否可读都是非阻塞的, 因此每次判断,不管有还是没有, 都会马上返回. 这样,任何一个 client server 发送字符或者试图建立新连接, 都不会对其他 client 的活动造成影响。

client 而言, 建立连接之后, 只需要处理两个文件描述符, 一个是建立了连接的 socket 描述符, 另一个是标准输入. server 一样, 如果使用阻塞方式的话, 很容易因为其中一个暂时没有输入而影响另外一个的读入.. 因此将它们都变成非阻塞的, 然后client 进行如下动作:

 

while ( 不想退出)

      begin

      if ( server 的连接有字符可读)

            begin

            从该连接读入, 并输出到标准输出上去.

            End;

      if ( 标准输入可读)

            Begin

            从标准输入读入, 并输出到与 server 的连接中去.

            End;

      End.

 

上面的读写分别调用这样两个函数:

read( userfd[i], line, MAX_LINE);

// userfd[i] 是指第 i client 连接的文件描述符.

// line 是指读出的字符存放的位置.

// MAX_LINE 是一次最多读出的字符数.

// 返回值是实际读出的字符数.

 

write( userfd[j], line, strlen( line));

// userfd[j] 是第 j client 的文件描述符.

// line 是要发送的字符串.

// strlen( line) 是要发送的字符串长度.

 

分析上面的程序可以知道, 不管是 server 还是 client, 它们都不停的轮流查询各个文件描述符, 一旦可读就读入并进行处理. 这样的程序, 不停的在执行, 只要有CPU 资源, 就不会放过。因此对系统资源的消耗非常大。server 或者 client 单独执行时, CPU 资源的 98% 左右都被其占用。极大的消耗了系统资源。

 

select 方法

 

      因此,虽然我们不希望在某一个用户没有反应时阻塞其他的用户,但我们却应该在没有任何用户有反应的情况之下停止程序的运行,让出抢占的系统资源,进入阻塞状态。有没有这种方法呢?现在的UNIX系统中都提供了select方法,具体实现方式如下:

 

      select 方法中, 所有文件描述符都是阻塞的. 使用 select 判断一组文件描述符中是否有一个可读(), 如果没有就阻塞, 直到有一个的时候就被唤醒. 我们先看比较简单的 client 的实现:

 

由于 client 只需要处理两个文件描述符, 因此, 需要判断是否有可读写的文件描述符只需要加入两项:

FD_ZERO( sockset);

// sockset 清空

FD_SET( sockfd, sockset);

// sockfd 加入到 sockset 集合中

FD_SET( 0, sockset);

// 0 (标准输入) 加入到 sockset 集合中

 

然后 client 的处理如下:

 

while ( 不想退出)

{

      select( sockfd+1,&sockset, NULL, NULL, NULL);

      // 此时该函数将阻塞直到标准输入或者 sockfd 中有一个可读为止

      // 第一个参数是 0 sockfd 中的最大值加一

      // 第二个参数是 读集, 也就是 sockset

      // 第三, 四个参数是写集和异常集, 在本程序中都为空

      // 第五个参数是超时时间, 即在指定时间内仍没有可读, 则出错

      // 并返回. 当这个参数为NULL , 超时时间被设置为无限长.

      // select 因为可读返回时, sockset 中包含的只是可读的

      // 那些文件描述符.

 

      if ( FD_ISSET( sockfd,&sockset))

      {

            // FD_ISSET 这个宏判断 sockfd 是否属于可读的文件描述符

            sockfd 中读入, 输出到标准输出上去.

      }

      if ( FD_ISSET( 0,&sockset))

      {

            // FD_ISSET 这个宏判断 sockfd 是否属于可读的文件描述符

            从标准输入读入, 输出到 sockfd 中去.

      }

      重新设置 sockset. (即将 sockset 清空, 并将 sockfd 0 加入)

}

 

下面看 server 的情况:

 

设置 sockset 如下:

FD_ZERO( sockset);

FD_SET( sockfd, sockset);

for ( 所有有效连接)

FD_SET( userfd[i], sockset);

}

maxfd = 最大的文件描述符号 + 1;

 

server 处理如下:

while ( 1)

{

      select( maxfd,&sockset, NULL, NULL, NULL);

      if ( FD_ISSET( sockfd,&sockset))

      {

            // 有新连接

            建立新连接, 并将该连接描述符加入到 sockset 中去了.

      }

      for ( 所有有效连接)

      {

            if ( FD_ISSET ( userfd[i],&sockset))

            {

                  // 该连接中有字符可读

                  从该连接中读入字符, 并发送到其他有效连接中去.

            }

      }

      重新设置 sockset;

}

 

性能比较

 

      由于采用 select 机制, 因此当没有字符可读时, 程序处于阻塞状态,最小程度的占用CPU 资源, 在同一台机器上执行一个 server 和若干个client , 系统负载只有 0.1 左右, 而采用原来的非阻塞通信方法, 只运行一个 server, 系统负载就可以达到 1.5 左右. 因此我们推荐使用 select.

 

参考文献:

[1] UNIX Network Programming Volume 1 W.Richard Stevens 1998 Prentice Hall

[2] 计算机实用网络编程 汤毅坚 1993 人民邮电出版社

[3] UNIX? SYSTEM V RELEASE 4 Programmer&aposs Guide:STREAMS AT&T 1990 Prentice Hall

[4] UNIX? SYSTEM V RELEASE 4 Network Programmer&aposs Guide AT&T 1990 Prentice Hall

所有源程序均登载在eDOC网站上,如有需要可以去下载

By Simon Lei, Jul.01,1999.

作者介绍:

姓名:雷云飞

笔名:eDOC工作组 联系地址: 安徽省合肥市四号信箱2331 230027

http://dev.csdn.net/develop/article/27/27235.shtm

 

 

 

 

WinSock I/O系列1:多路复用I/O支持多Client的实现及效率讨论

 

关键字   多路复用I/O select效率 多客户端

出处  

 

 1.       引言

 

多路复用I/O模型(select)UNIX/LINUX用得的最多的一种I/O模型,在Windows下也

 

可做为一种同步I/O使用。本文给出该I/O模型处理多Client的简单(在主线程中)实现。

 

2.       关于select

 

select I/O模型是一种异步I/O模型,在单线程中Linux/WinNT默认支持64个客户端套

 

接字。这种I/O模型主要涉及以下几个函数及宏:

 

int select()FD_ZEROFD_SETFD_ISSET以及FD_SETSIZE

 

3.       select开发一个Server

 

3.1 只支持单个Client

 

    // 相关初始化处理, 创建监听套接字

 

    listen(listensock,  5);

 

    clientsock  =  accept(listensock,  NULL,  NULL);

 

    for  (; ;)

 

    {

 

             FD_ZERO(&readfds);

 

             FD_SET(clientsock,&readfds);

 

             nResult = select(

 

                     0,         // Windows中这个参数忽略,Linux中在此处为1

 

                     readfds,    // 可读套接字集合

 

                     ……

 

              )

 

             if   (nResult  = =  SOCKET_ERROR)

 

                    return –1;

 

             // 判断cliensock是否处于待读状态

 

             if  (FD_ISSET(clientsock, &readfds))

 

            {

                              // 相关处理

 

            }

 

    }

 

其实Winsock中的WSAEventSelect模型是与之类似的。

 

3.2  在单线程中支持63Client

 

   SOCKET clientsockarray[FD_SETSIZE – 1];   // FD_SETSIZE is 64

 

  // 相关初始化处理, 创建监听套接字

 

  

 

  listen(listensock, 5);

 

  // 初始化套接字数组

 

 InitSock(clientsockarray);

 

  FD_ZERO(&readfds);

 

  FD_SET(listensock, &readfds);

 

  for  (; ;)

 

 {

 

 nRet  = select(0, &readfds,  NULL,  NULL,  NULL);

 

// 判断监听套接字是否可读

 

 if  (FD_ISSET(listensock, &readfds))

 

 {

 

         clientsock = accept(listensock,  NULL,  NULL);

 

          // 将客户套接字放到套接字数组中

 

          if   (!InsertSock(clientsockarray, clientsock))

 

          {

 

                   printf("客户端超过了63,此次连接被拒绝.\n");

 

                   closesocket(clientsock);

 

                   continue;

 

           }  

 

  }

 

  

 

  // 逐个处理处于待决状态的套接字

 

  for  (nIndex  =  0;  nIndex <  FD_SETSIZE  -  1;  nIndex++)

 

 {

 

           if   (FD_ISSET(clientsockarray[nIndex], &readfds))

 

           {

 

                     nRet  =  recv(clientsockarray[nIndex],  buff,  sizeof(buff),  0);

 

                     if  (nRet  = =  0  ||  nRet  = =  SOCKET_ERROR)

 

                     {

 

                                closesocket(clientsockarray[nIndex]);

 

                            clientsockarray[nIndex] = INVALID_SOCKET;

 

                            continue;       // 继续处理套接字句柄数组中的其它套接字

 

                     }

 

                     // 将接受到的数据进行处理,此处只将其输出

 

                     printf("%s\n", buff);

 

              }

 

       }

 

 

 

       // 初始化套接字集合

 

       FD_ZERO(&readfds);

 

       FD_SET(listensock,&readfds);

 

       // 将所有有效的套接字句柄加入到套接字句柄数组中

 

       for (nIndex = 0; nIndex< FD_SETSIZE - 1; nIndex++)

 

       {

 

if (clientsockarray[nIndex] != INVALID_SOCKET)

 

              FD_SET(clientsockarray[nIndex],&readfds);

 

       }

 

}

 

 

 

BOOL InsertSock(SOCKET* pSock,  SOCKET sock)

 

{

 

          for   (int  nIndex  =  0;  nIndex <  FD_SETSIZE – 1;  nIndex++)

 

         {

                                     if   (pSock[nIndex]  = =  INVALID_SOCKET)

 

                  {

 

                          pSock[nIndex] = sock;

 

                          break;

 

                  }

 

          }

 

 

 

          if   (nIndex = = FD_SETSIZE – 1)

 

                 return FALSE;

 

    

 

          return TRUE;

 

 }

 

 

 

       上面只是给简要的代码,有的辅助函数也没有给出。用select支持多Client是比较方便的,在一个线程中可支持63个;可以采用多线程支持更大数量的Client

 

4.       效率的讨论

 

4.1  对套接字数组扫描的效率问题

 

    在上面的程序中,存在多处对套接字句柄的扫描处理,这个肯定会影响效率。不知道各位朋友是怎么处理这个问题的。

 

4.2 对客户端实时响应问题

 

上面的程序处理待决的套接字的时候,是逐个处理的,如果响应某个Client的时间长到一定程度的话,肯定会影响对其它客户端的响应。我的解决方法是当这个套接字处于可读的待决状态的话,产生一个子线程去处理------接收数据和处理数据。这样主线程继续自己的工作,其它Client可以得及时的响应;当然当有大量的Client请求时,对线程的控制会成为一个新问题。

 

UNIX/LINUX下做一个支持大量ClientServer的话,本人还是最先选择select这种I/O模型,这是因为我还不知道LINUX还有哪些更好的I/O模型。WinNT的话,还有CompletionPortOverlapped,特别对于有大数据量传送,同时只有少量的Client时,Overlapped可以发挥相当大的作用。各位朋友请给出使用select的好方法。

阅读(771) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~