纯真首页 - IT - Linux

Linux socket 编程入门 TCP server 端(2)

 
日期:2009-6-12
正常  大字体 ]


5、创建监听嵌套字 作者:龙飞

        前面一小节,我们已经写出了TcpServer的构造函数。这个函数的实际作用,就是创建了listen socket(监听嵌套字)。这一节,我们来具体分析这个创建的过程。

socket和sockaddr的创建是可以相互独立的

        在函数中,我们首先通过socket()系统调用创建了listenSock,然后通过为结构体赋值的方法具体定义了服务器端的 sockaddr。(memset()函数的作用是把某个内存段的空间设定为某值,这里是清零。)其他的概念已经在前一小节讲完了。这里需要补充的是说明宏定义INADDR_ANY。这里的意思是使用本机所有可用的IP地址。当然,如果你机器绑定了多个IP地址,你也可以指定使用哪一个。

数据流简易模型(SOCK_STREAM)

        我们的例子以电话做的比喻,实际上,socket stream模型不完全类似电话,它至少有以下这些特点:
1、一种持续性的连接。这点跟电话是类似的,也可以想象成流动着液体的水管。一旦断开,这种流动就会中断。
2、数据包的发送实际上是非连续的。这个世界上有什么事物是真正的线性连续的?呵呵,扯远了,这貌似一个哲学问题。我们仅仅需要知道的是,一个数据包不可能是无限大的,所以,总是一个小数据包一个小数据包这样的发送的。这一点,又有点像邮包的传递。这些数据包到达与否,到达的先后次序本身是无法保证的,即是说,是IP协议无法保证的。但是stream形式的TCP协议,在IP之上,做了一定到达和到达顺序的保证。
3、传送管道实际上是非封闭的。要不干嘛叫“网络”-_-!!!。我们之所以能保证数据包的“定点”传送,完全是依靠每个数据包都自带了目的地址信息。
        由此可见,虽然socket和sockaddr可以分别创建,并无依赖关系。但是在实际使用的时候,一个socket至少会绑定一个本机的sockaddr,没有自己的“地址信息”,就不能接受到网络上的数据包(至少在TCP协议里面是这样的)。

socket与本机sockaddr的绑定

        有时候绑定是系统的任务,特别是当你不需要知道自己的IP地址和所使用的端口号的时候。但是,我们现在是建立服务器,你必须告诉客户端你的连接信息:IP和Port。所以,我们需要指明IP和Port,然后进行绑定。
int  bind( int  socket,  struct  sockaddr *  localAddress, unsigned  int  addressLength);

作为C++的程序员,也许你会觉得这个函数很不友好,它似乎更应该写成:
int  bind_cpp_style( int  socket,  const  sockaddr &  localAddress);

我们需要通过函数原型指明两点:
1、我们仅仅使用sockaddr结构的数据,但并不会对原有的数据进行修改;
2、我们使用的是完整的结构体,而不仅仅是这个结构体的指针。(很显然光用指针是无法说明结构体大小的)
幸运的是,在Linux的实现中,这个函数已经被写为:
#include  < sys / socket.h >

/*  Give the socket FD the local address ADDR (which is LEN bytes long).   */
extern   int  bind ( int  __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)
     __THROW;

看到亲切的const,我们就知道这个指针带入是没有“副作用”的。

监听:listen()

        stream流模型形式上是一种“持续性”的连接,这就是要求信息的流动是“可来可去”的。也就是说,stream流的socket除了绑定本机的 sockaddr,还应该拥有对方sockaddr的信息。在listen()中,这“对方的sockaddr”就可以不是某一个特定的 sockaddr。实际上,listen socket的目的是准备被动的接受来自“所有”sockaddr的请求。所以,listen()反而就不能指定某个特定的sockaddr。
int  listen( int  socket,  int  queueLimit);

其中第二个参数是等待队列的限制,一般设置在5-20。Linux中实现为:
#include  < sys / socket.h >

/*  Prepare to accept connections on socket FD.
   N connection requests will be queued before further requests are refused.
   Returns 0 on success, -1 for errors.   */
extern   int  listen ( int  __fd,  int  __n) __THROW;

完成了这一步,回到我们的例子,就像是让你小弟在电话机前做好了接电话的准备工作。需要再次强调的是,这些行为仅仅是改变了socket的状态,实际上我想强调的是,为什么这些函数不会造成block(阻塞)的原因。(block的概念以后再解释)

6、创建“通讯 ”嵌套字 作者:龙飞

        这里的“通讯”加上了引号,是因为实际上所有的socket都有通讯的功能,只是在我们的例子中,之前那个socket只负责listen,而这个socket负责接受信息并echo回去。
 我们现看看这个函数:
bool  TcpServer::isAccept()
{
    unsigned  int  clntAddrLen  =   sizeof (clntAddr);

     if  ( (communicationSock  =  accept(listenSock, (sockaddr * ) & clntAddr,  & clntAddrLen))  <   0  ) {
         return   false ;
    }  else  {
        std::cout  <<   " Client(IP:  "   <<  inet_ntoa(clntAddr.sin_addr)  <<   " ) connected.\n " ;
         return   true ;
    }
}


用accept()创建新的socket

        在我们的例子中,communicationSock实际上是用函数accept()创建的。
int  accept( int  socket,  struct  sockaddr *  clientAddress, unsigned  int *  addressLength);

在Linux中的实现为:
/*  Await a connection on socket FD.
   When a connection arrives, open a new socket to communicate with it,
   set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
   peer and *ADDR_LEN to the address's actual length, and return the
   new socket's descriptor, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.   */
extern   int  accept ( int  __fd, __SOCKADDR_ARG __addr,
           socklen_t  * __restrict __addr_len);

这个函数实际上起着构造socket作用的仅仅只有第一个参数(另外还有一个不在这个函数内表现出来的因素,后面会讨论到),后面两个指针都有副作用,在socket创建后,会将客户端sockaddr的数据以及结构体的大小传回。
        当程序调用accept()的时候,程序有可能就停下来等accept()的结果。这就是我们前一小节说到的block(阻塞)。这如同我们调用 std::cin的时候系统会等待输入直到回车一样。accept()是一个有可能引起block的函数。请注意我说的是“有可能”,这是因为 accept()的block与否实际上决定与第一个参数socket的属性。这个文件描述符如果是block的,accept()就block,否则就不block。默认情况下,socket的属性是“可读可写”,并且,是阻塞的。所以,我们不修改socket属性的时候,accept()是阻塞的。

accept()的另一面connect()

        accept()只是在server端被动的等待,它所响应的,是client端connect()函数:
int  connect( int  socket,  struct  sockaddr *  foreignAddress, unsigned  int  addressLength);

虽然我们这里不打算详细说明这个client端的函数,但是我们可以看出来,这个函数与之前我们介绍的bind()有几分相似,特别在Linux的实现中:
/*  Open a connection on socket FD to peer at ADDR (which LEN bytes long).
   For connectionless socket types, just set the default address to send to
   and the only address from which to accept transmissions.
   Return 0 on success, -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.   */
extern   int  connect ( int  __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);

connect() 也使用了const的sockaddr,只不过是远程电脑上的而非bind()的本机。
        accept()在server端表面上是通过listen socket创建了新的socket,实际上,这种行为是在接受对方客户机程序中connect()函数的请求后发生的。综合起看,被创建的新 socket实际上包含了listen socket的信息以及客户端connect()请求中所包含的信息——客户端的sockaddr地址。

新socket与sockaddr的关系

        accept()创建的新socket(我们例子中的communicationSock,这里我们简单用newSock来带指)首先包含了listen socket的信息,所以,newSock具有本机sockaddr的信息;其次,因为它响应于client端connect()函数的请求,所以,它还包含了clinet端sockaddr的信息。
        我们说过,stream流形式的TCP协议实际上是建立起一个“可来可去”的通道。用于listen的通道,远程机的目标地址是不确定的;但是 newSock却是有指定的本机地址和远程机地址,所以,这个socket,才是我们真正用于TCP“通讯”的socket。

inet_ntoa()
#include  < arpa / inet.h >

/*  Convert Internet number in IN to ASCII representation.  The return value
   is a pointer to an internal array containing the string.   */
extern   char   * inet_ntoa ( struct  in_addr __in) __THROW;

        对于这个函数,我们可以作为一种,将IP地址,由in_addr结构转换为可读的ASCII形式的固定用法。

 7、接收与发送 作者:龙飞

        现在,我们通过accept()创建了新的socket,也就是我们类中的数据成员communicationSock,现在,我们就可以通过这个socket进行通讯了。

TCP通讯模型

        在介绍函数之前,我们应该了解一些事实。TCP的Server/Client模型类似这样:
ServApp——ServSock——Internet——ClntSock——ClntApp
当然,我们这里的socket指的就是用于“通讯”的socket。TCP的server端至少有两个socket,一个用于监听,一个用于通讯;TCP的 server端可以只有一个socket,这个socket同时“插”在server的两个socket上。当然,插上listen socket的目的只是为了创建communication socket,创建完备后,listen是可以关闭的。但是,如果这样,其他的client就无法再连接上server了。
        我们这个模型,是client的socket插在server的communication socket上的示意。这两个socket,都拥有完整的本地地址信息以及远程计算机地址信息,所以,这两个socket以及之间的网络实际上形成了一条形式上“封闭”的管道。数据包只要从一端进来,就能知道出去的目的地,反之亦然。这正是TCP协议,数据流形式抽象化以及实现。因为不再需要指明“出处” 和“去向”,对这样的socket(实际上是S/C上的socket对)的操作,就如同对本地文件描述符的操作一样。但是,尽管我们可以使用read() 和write(),但是,为了完美的控制,我们最好使用recv()和send()。

recv()和send()
int  send( int  socket,  const   void *  msg, unsigned  int  msgLength,  int  flags);
int  recv( int  socket,  void *  rcvBuffer, unsigned  int  bufferLength,  int  flags);

在Linux中的实现为:
#include  < sys / socket.h >

/*  Send N bytes of BUF to socket FD.  Returns the number sent or -1.

   This function is a cancellation point and therefore not marked with
   __THROW.   */
extern  ssize_t send ( int  __fd, __const  void   * __buf, size_t __n,  int  __flags);

/*  Read N bytes into BUF from socket FD.
   Returns the number read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.   */
extern  ssize_t recv ( int  __fd,  void   * __buf, size_t __n,  int  __flags);

这两个函数的第一个参数是用于“通讯”的socket,第二个参数是发送或者接收数据的起始点指针,第三个参数是数据长度,第四个参数是控制符号(默认属性设置为0就可以了)。失败时候传回-1,否则传回实际发送或者接收数据的大小,返回0往往意味着连接断开了。

处理echo行为
void  TcpServer::handleEcho()
{
     const   int  BUFFERSIZE  =   32 ;
     char  buffer[BUFFERSIZE];
     int  recvMsgSize;
     bool  goon  =   true ;

     while  ( goon  ==   true  ) {
         if  ( (recvMsgSize  =  recv(communicationSock, buffer, BUFFERSIZE,  0 ))  <   0  ) {
             throw   " recv() failed " ;
        }  else   if  ( recvMsgSize  ==   0  ) {
            goon  =   false ;
        }  else  {
             if  ( send(communicationSock, buffer, recvMsgSize,  0 )  !=  recvMsgSize ) {
                 throw   " send() failed " ;
            }
        }
    }

    close(communicationSock);
}

本小节最后要讲的函数是close(),它包含在<unistd.h>中
#include  < unistd.h >

/*  Close the file descriptor FD.

   This function is a cancellation point and therefore not marked with
   __THROW.   */
extern   int  close ( int  __fd);

这个函数用于关闭一个文件描述符,自然,也就可以用于关闭socket。
下一小节是完整的源代码。默认的监听端口是5000。我们可以通过
$telnet 127.0.0.1 5000
验证在本机运行的echo server程序。

8、本章的完整源代码
// Filename: TcpServerClass.hpp

#ifndef TCPSERVERCLASS_HPP_INCLUDED
#define  TCPSERVERCLASS_HPP_INCLUDED

#include  < unistd.h >
#include  < iostream >
#include  < sys / socket.h >
#include  < arpa / inet.h >

class  TcpServer
{
private :
     int  listenSock;
     int  communicationSock;
    sockaddr_in servAddr;
    sockaddr_in clntAddr;
public :
    TcpServer( int  listen_port);
     bool  isAccept();
     void  handleEcho();
};


#endif   //  TCPSERVERCLASS_HPP_INCLUDED
// Filename: TcpServerClass.cpp

#include  " TcpServerClass.hpp "

TcpServer::TcpServer( int  listen_port)
{
     if  ( (listenSock  =  socket(PF_INET, SOCK_STREAM, IPPROTO_TCP))  <   0  ) {
         throw   " socket() failed " ;
    }

    memset( & servAddr,  0 ,  sizeof (servAddr));
    servAddr.sin_family  =  AF_INET;
    servAddr.sin_addr.s_addr  =  htonl(INADDR_ANY);
    servAddr.sin_port  =  htons(listen_port);

     if  ( bind(listenSock, (sockaddr * ) & servAddr,  sizeof (servAddr))  <   0  ) {
         throw   " bind() failed " ;
    }

     if  ( listen(listenSock,  10 )  <   0  ) {
         throw   " listen() failed " ;
    }
}

bool  TcpServer::isAccept()
{
    unsigned  int  clntAddrLen  =   sizeof (clntAddr);

     if  ( (communicationSock  =  accept(listenSock, (sockaddr * ) & clntAddr,  & clntAddrLen))  <   0  ) {
         return   false ;
    }  else  {
        std::cout  <<   " Client(IP:  "   <<  inet_ntoa(clntAddr.sin_addr)  <<   " ) connected.\n " ;
         return   true ;
    }
}

void  TcpServer::handleEcho()
{
     const   int  BUFFERSIZE  =   32 ;
     char  buffer[BUFFERSIZE];
     int  recvMsgSize;
     bool  goon  =   true ;

     while  ( goon  ==   true  ) {
         if  ( (recvMsgSize  =  recv(communicationSock, buffer, BUFFERSIZE,  0 ))  <   0  ) {
             throw   " recv() failed " ;
        }  else   if  ( recvMsgSize  ==   0  ) {
            goon  =   false ;
        }  else  {
             if  ( send(communicationSock, buffer, recvMsgSize,  0 )  !=  recvMsgSize ) {
                 throw   " send() failed " ;
            }
        }
    }

    close(communicationSock);
}


演示程序:
// Filename: main.cpp
// Tcp Server C++ style, single work

#include  < iostream >
#include  " TcpServerClass.hpp "

int  echo_server( int  argc,  char *  argv[]);

int  main( int  argc,  char *  argv[])
{
     int  mainRtn  =   0 ;
     try  {
        mainRtn  =  echo_server(argc, argv);
    }
     catch  (  const   char *  s ) {
        perror(s);
        exit(EXIT_FAILURE);
    }

     return  mainRtn;
}

int  echo_server( int  argc,  char *  argv[])
{
     int  port;
     if  ( argc  ==   2  ) {
        port  =  atoi(argv[ 1 ]);
    }  else  {
        port  =   5000 ;
    }

    TcpServer myServ(port);

     while  (  true  ) {
         if  ( myServ.isAccept()  ==   true  ) {
            myServ.handleEcho();
        }
    }

     return   0 ;

软件搜索

 
版权所有 Copyright © 2003-2017  纯真网络   联系方式
如有任何问题和建议,请联系我们。EMail: admin@cz88.net
粤ICP备12084360号-2 穗公网监备案证第4401040400001号