zoukankan      html  css  js  c++  java
  • 基于socket的聊天室实现原理

    基于socket的聊天室,目前还比较少见,国内比较知名的有网易和碧海银沙聊天室。这种聊天室的特点很明显,不象CGI聊天室那样不管有没有人发言,都会定时刷新。而是当有人发言时,屏幕上才会出现新聊天内容,而且聊天内容是不断向上滚动的,如果浏览器状态栏在的话,可以看到进度条始终处于下载页面状态。这种聊天室可以容纳许多人而性能不会明显降低,象网易聊天室经常有数百人在一台服务器上聊天。由于这种方式不同于CGI聊天室由客户端浏览器定时请求聊天内容,而是由聊天服务器软件向客户浏览器主动发送信息。
    Socket聊天室基本原理是,抛开cgi和www服务器,根据html规范,接收到浏览器的请求以后,模仿www服务器的响应,将聊天内容发回浏览器。在浏览器看来就象浏览一个巨大的页面一样始终处于页面联接状态。实际上就是一个专门的聊天服务器,一个简化了的www服务器。
    这样相比CGI方式来说,Socket聊天室的优点就很明显:
    1. 不需要专门的WWW Server,在聊天服务器里完成必要的工作,避开耗时的CGI过程
    2. 如果使用单进程服务器,就不需要每次产生新进程
    3. 数据交换完全在内存进行,不用读写文件
    4. 不需要定时刷新,减少屏幕的闪烁,减少对服务器的请求次数

    在讨论具体流程之前,我们先来讨论相关的一些技术:

    http请求和应答过程
    http协议是浏览器与www服务器之间通信的标准,作为一个简化了的www服务器,socket聊天服务器应当遵守这个协议。实际上只要实现一小部分就可以了。
    http使用了客户服务器模式,其中浏览器是http客户,浏览某个页面实际上就是打开一个连接,发送一个请求到www服务器,服务器根据所请求的资源发送应答给浏览器,然后关闭连接。客户和服务器之间的请求和应答有一定的格式要求,只要按照这个格式接收请求发送应答,就可以“欺骗”浏览器,使它以为正在与www服务器通信。
    请求和应答具有类似的结构,包括:
    · 一个初始行
    · 0个或多个header lines
    · 一个空行
    · 可选的信息
    我们看看一个浏览器发出的请求:
    当我们浏览网页:http://www.somehost.com/path/file.html的时候,浏览器首先打开一个到主机www.somehost.com的80端口的socket,然后发送以下请求:
    GET /path/file.html HTTP/1.0
    From: someuser@somehost.com
    User-Agent: Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 4.0; DigExt)
    [空行]

    第一行GET /path/file.html HTTP/1.0是我们需要处理的核心。由以空格分隔的三部分组成,方法(method):GET,请求资源:/path/file.html,http版本:HTTP/1.0。

    服务器将会通过同一个socket用以下信息回应:
    HTTP/1.0 200 OK
    Date: Fri, 31 Dec 1999 23:59:59 GMT
    Content-Type: text/html
    Content-Length: 1354

    <html>
    <body>
    <h1>Hello world!</h1>
    (其他内容)
    .
    .
    .
    </body>
    </html>
    第一行同样也包括三部分:http版本,状态码,与状态码相关的描述。状态码200表示请求成功。
    发送完应答信息以后,服务器就会关闭socket。
    以上过程可以利用telnet www.somehost.com:80来模拟。

    循环服务器和并发服务器
    循环服务器是一个时刻只能处理一个请求的服务器,多个请求同时到来将会放在请求队列里。
    而并发服务器则是在每个请求到来以后分别产生一个新进程来处理。
    并发服务器由于其算法而具有与生俱来的快速响应优势,而且当某一个用户与服务器通信死锁不会影响其他进程,但由于多个进程之间需要通过进程间通信实现信息交换,而且fork新进程所带来的开销随着用户数量的增加越来越大,因此并发服务器在某些情况下不一定是最佳选择。
    循环服务器虽然表面上会产生时延,但是象聊天室这样的系统实际上处理每个请求的过程非常的短,对客户而言,可以取得与并发服务器一样的效果,而且由于是单进程服务器,不需要进程间通信,不需要fork新进程,编程简单,系统资源消耗极少。但由于是单进程,某个客户与服务器之间死锁将导致整个系统死锁。

    POST与GET
    提交form信息一般常用的有两种:POST & GET,POST由于长度不受限制,而作为大多数form提交时使用的方法。GET方法通过URL来发送提交信息,由于URL最长只能1024字节,所以如果发送信息很长的话,不能使用这种方法。由于聊天内容有长度限制,不会很长,而且因为普通浏览页面使用GET方法,因此使用GET方法提交form表单,可以简化处理过程。

    使用perl模块实现Socket通信
    假定您对socket编程有一定的了解,如果您使用过C语言进行过socket编程,那么理解perl语言socket编程将是一件非常容易的事。如果不熟悉socket,请看本文所附socket参考。
    使用perl编写socket程序可以通过use Socket,也可以通过use IO::Socket。前一种方法接近于C语言,后一种则进行了对象封装,编写维护会容易许多。

    我们在通过单进程循环服务器实现并发服务的时候,基本思路是:允许多个客户打开到服务器的socket连接,服务器通过一定的方法监测哪些socket有数据到达,并处理该连接。在这个思路中有个关键问题服务器如何触发数据处理?了解C语言socket编程就会知道有个系统函数select可以完成这一操作,但由于使用了位操作,perl语言处理不是很清晰,但是如果使用了模块IO::Select就会变得很简单。
    我们看一个来自IO::Select的帮助的例子:
    use IO::Select;
    use IO::Socket;

    $lsn = new IO::Socket::INET(Listen => 1, LocalPort => 8080);
    #创建socket,在端口 8080上监听,相当于使用系统函数
    #socket(),bind(),listen()

    $sel = new IO::Select( $lsn );
    #创建select对象,并把前面创建的socket对象加入

    while(@ready = $sel->can_read) {#处理每个可读的socket
    foreach $fh (@ready) {
    if($fh == $lsn) {
    #如果最初创建的socket可读,说明有新连接
    #建立一个新的socket,加入select
    $new = $lsn->accept;
    $sel->add($new);
    }
    else {
    #如果是其他socket,读取数据,处理数据
    ……
    #处理完成以后,从select中删除socket,然后关闭socket
    $sel->remove($fh);
    $fh->close;
    }
    }
    }
    IO::Socket的基本操作,
    创建socket对象:$socket=new IO::Socket::INET();
    接收客户的连接请求:$new_socket=$socket->accept;
    通过socket发送数据:$socket->send($message);
    从socket接收数据:$socket->recv($buf,LENGTH);
    关闭socket连接:$socket->close;
    判断socket是否出于打开状态:$socket->opened;

    IO::Select的基本操作
    创建select对象:$select=new IO::Select();
    添加socket到select中:$select->add($new_socket);
    从select中删除socket:$select->remove($old_socket);
    从select中查找可读的socket:@readable=$select->can_read;
    找出select中的所有socket:@sockets=$select->handles;

    Daemon实现方法
    实现一个后台进程需要完成一系列的工作,包括
    · 关闭所有的文件描述字
    · 改变当前工作目录
    · 重设文件存取屏蔽码(umask)
    · 在后台执行
    · 脱离进程组
    · 忽略终端I/O信号
    · 脱离控制终端
    这些操作可以利用perl模块来简化:
    use Proc::Daemon;
    Proc::Daemon::Init;


    pipe信号处理
    如果客户关闭了socket以后,服务器继续发送数据,将会产生PIPE Signal,如果不加处理,就会导致服务器意外中断,为避免这一情况的发生,我们必须对它进行处理,一般情况下,只需要简单地忽略这个信号即可。
    $SIG{‘PIPE’}=’IGNORE’;

    意外处理
    在Socket通信过程中很容易出现一些意外情况,如果不加处理直接发送数据,就可能导致程序意外退出。Perl语言中的eval函数可以用于意外处理。例如:
    if (!defined(eval{操作语句;})){
    错误处理;
    }
    这样当eval中的操作语句出现错误,如die的时候,只会中止eval语句,并不会中断主程序。

    用户断线判断和处理
    许多情况下,用户不是通过提交“离开”按钮离开聊天室,这时候就需要判断用户是否断线了。方法是:当用户关闭浏览器,或者点击了浏览器stop按钮,或者跳转到其他网页的时候,相对应的socket将会变成可读状态,而此时读出的数据却是空字符串。
    利用这个原理,只要在某个可读的socket读取数据时,读到的却是空数据,那么我们就可以断定,与这个socket相对应的用户断线了。

    防止用户断线
    如果浏览器在一段时间内没有接到任何数据,那么就会出现超时错误。要避免这一错误,必须在一定间隔内发送一些数据,在我们这个应用系统里,可以发送一些html注释。发送注释的工作可以由在线名单刷新过程顺带完成。

    下面我们来看看具体实现流程:
    聊天服务器实现流程
    · 服务器端
    下图是NS盒图程序流程:


    上图中的“处理用户输入”部分可以细化为下图:

    用户数据输入都是通过URL传送,下面是几个url实例,结合后面客户端流程,可以更好地理解系统结构:
    这是一个用户名密码均为’aaa’的聊天用户登录系统,说了一句话“hello”,然后退出所产生的一系列请求,其中密码用系统函数crypt加密过:
    /login?name=aaa&passwd=PjHIIEleipsEE
    /chat?sid=ZUyPHh3TWhENKsICnjOv&passwd=PjHIIEleipsEE
    /talk?sid=ZUyPHh3TWhENKsICnjOv&passwd=PjHIIEleipsEE
    /names?sid=ZUyPHh3TWhENKsICnjOv
    /doTalk?sid=ZUyPHh3TWhENKsICnjOv&passwd=PjHIIEleipsEE&message=hello
    /leave?sid=ZUyPHh3TWhENKsICnjOv&passwd=PjHIIEleipsEE

    以上是服务器程序流程,下面我们从客户端看看具体登录过程。
    我们先看看聊天界面:


    聊天界面由三个frame组成,其中chat帧是聊天内容显示部分;talk帧是用户输入部分,包括聊天内容输入、动作、过滤以及管理功能都在这一帧输入;names是在线名单显示部分,这一部分是定时刷新的。

    让我们从浏览器的角度来看看进入聊天室的过程。
    · 首先浏览器请求页面
    http://host:9148/login?name=NAME&passwd=PWD
    此时产生了一个连接到服务器聊天端口的socket联接,并发送了一行数据:
    GET /login?name=NAME&passwd=PWD HTTP/1.1
    · 服务器生成一个session ID,验证密码以后,发回:
    HTTP/1.1 200 OK
    <其他头信息>
    Content-TYPE: text/html
    <空行>
    <html>
    ……
    <frameset cols="*,170" rows="*" border="1" framespacing="1">
    <frameset rows="*,100,0" cols="*" border="0" framespacing="0">
    <frame src="/chat?sid=$sid&passwd=$encrypt_pass" name="u" frameborder="NO" noresize>
    <frame src="/talk?sid=$sid&passwd=$encrypt_pass" name="d" frameborder="NO" noresize>
    </frameset>
    <frame src="/names?sid=$sid" name="r" noresize>
    </frameset>
    ……
    </html>
    然后服务器关闭socket联接。

    · 浏览器收到以上html文件后,将会依次打开三个联接(其中的$sid和$encrypt_pass是变量):
    /chat?sid=$sid&passwd=$encrypt_pass /talk?sid=$sid&passwd=$encrypt_pass
    /names?sid=$sid
    这三个联接中的第一个联接chat在整个聊天过程中都是保持联接的,这样从浏览器角度来看,就是一个始终下载不完的大页面,显示效果上就是聊天内容不是靠刷新来更新,而是不断地向上滚动。通过察看html代码可以看到,只有<html><body>,然后就是不断增加的聊天内容,没有</body></html>。
    另外两个联接在页面发送完毕以后,socket就关闭了。
    这样一次登录聊天室实际上有四次socket联接,但登录完成以后,只有chat帧的socket是保持联接的,用于接收来自服务器的聊天信息,这是socket聊天室的关键所在。
    在服务器端储存了所有参加聊天的客户的chat socket,当有人发言时,服务器就向所有chat socket发送聊天内容。
    Talk与names帧的html实际上和普通的form是一样的。

    · 在用户登录以后,服务器端保存了一张包括用户信息的表格。
    在perl实现中,我们使用哈希结构储存信息,以session ID作为key索引。这样的存储结构便于存取数据,回收空间。每个客户信息是一个数组:
    [socket,name,passwd,privilige,filter,login_time,color]
    socket:储存chat帧socket联接
    name:用户名
    passwd:密码
    privilige:权限
    filter:某个用户的过滤列表的引用(reference)
    login_time:记录登录时间,以便以后清除一些超时联接
    color:用户聊天颜色
    以上用户数据大部分是在login阶段,用户通过密码验证以后填入的。只有chat socket要等到chat帧显示以后才得到。如果超过一定时间,socket还是没有填入,说明浏览器取得主框架以后连接中断了,这时候就需要删除该用户数据。

    以上是聊天室核心部分,其他部分,如用户注册、改密码等可以沿用CGI聊天室代码。

    需要改进的地方
    目前提供了聊天、悄悄话、动作这些基本聊天功能以及过滤用户名单这样的附加功能。管理功能完成了踢人、查IP、任命室主。今后需要改进的地方有:
    稳定性:目前聊天室还没有经过大用户量测试,稳定性还不能充分保证。由于是单进程循环服务器,某个用户通信死锁将导致所有人死锁。如果采用并发多进程服务器,可以使稳定性得到提高。但这样的系统对服务器资源消耗也会大许多。
    功能:自建聊天室等功能还没有完成,这些外围功能在稳定性有保证以后就可以比较容易地加入。

    [参考内容]
    1. 本文所述的聊天室的最初结构来自于Entropy Chat 2.0(http://missinglink.darkorb.net/pub/entropychat/),如果没有它的启示,完成这一系统会有许多困难,非常感谢他们的努力工作,愿意共同完善这个程序的朋友们,可以到http://tucows.qz.fj.cn/chat下载源代码。

    2. http的基本交互过程请参考
    HTTP Made Really Easy(http://www.jmarshall.com/easy/http/),RFC1945:Hypertext Transfer Protocol -- HTTP/1.0

    3. 本文所提到的perl模块,都可以在http://tucows.qz.fj.cn找到,请使用页面上方的搜索功能搜索。
    IO::Socket和IO::Select是perl标准模块,也可以通过安装IO-1.20.tar.gz得到。
    Proc:Daemon需要另外安装,模块为Proc-Daemon-0.02.tar.gz
    上述模块版本号可能有所不同,搜索时只要输部分关键字如:”Daemon”即可找到。

    4. 为加快开发过程,程序的界面部分参考了网易聊天室(http://chat.163.net/),程序的很多想法也来自于他们的工作。

    5. 《How to Write a Chat Server》可以作为一个很好的参考
    http://hotwired.lycos.com/webmonkey/97/18/index2a.html

    6. 需要测试聊天室功能可以到http://tucows.qz.fj.cn/chat;
    7. socket编程参考
    · Unix Socket FAQ(http://www.ntua.gr/sock-faq/)
    · Beejs Guide to Network Programming
    (http://www.ecst.csuchico.edu/~beej/guide/net/


     

  • 相关阅读:
    disruptor和ArrayBlockingQueue和LinkedBlockingQueue队列性能对比
    守护线程的作用和前台线程的区别
    tomcat导入idea作为maven项目
    百度网盘不限速
    netty ChannelOption参数 backlog 和 somaxconn同时设置才会生效
    dubbo的初探
    IDEA的常用快捷键
    Lucene简单了解和使用
    Hadoop的简单了解与安装
    Nginx的简单了解与使用
  • 原文地址:https://www.cnblogs.com/bluespot/p/1114479.html
Copyright © 2011-2022 走看看