Document

WebSocket 网页聊天室的实现(服务器端:.net + windows服务,前端:Html5)

websocket是HTML5中的比较有特色一块,它使得以往在客户端软件中常用的socket在web程序中也能轻松的使用,较大的提高了效率。废话不多说,直接进入题。

网页聊天室包括2个部分,后端服务器+前端页面。 
1、后端服务部分:.net4.0 + windows服务。相比寄宿在iis中,寄宿在进程中的windows服务更加的稳定可靠(文章中的例子用windows控制台程序演示,后面给出完整的windows服务的代码)。 
2、前端部分:html5 + jQuery + bootstrap。基本的前端快速开发利器。

一、分析一下聊天室的场景需求,以便构建合适的数据结构

1、在线用户类 OnlineUser 用户的基本特征为姓名Name(性别年龄啥的先忽略),当然在系统设计中,姓名并不能很好的区分不同用户,所以得需要一个唯一标识符Id。另外,由于可能存在多个聊天室,因此聊天室的编号RoomId也是用户的特征之一。综上,可得在线用户类OnlineUser的结构为:

///  <summary>
///  在线用户信息
///  <summary>
public class OnlineUser
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string RoomId { get; set; }
    public string SessionId { get; set; }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

PS:SessionId是后续加入的属性,此处可先忽略。

2、消息类 Message 消息是聊天室最核心的部分,我最简单的消息机构应包含如下成员,消息发送者FromUserId,消息接受者ToUserId,消息类型Type,消息内容Content,消息时间Time。

public class Message
{
    public int FromUserId { get; set; }
    public int ToUserId { get; set; }
    public int Type { get; set; }
    public string Time { get; set; }
    public string Content { get; set; }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

PS:这里Type用的是int类型表示的,是出于后续消息传递时,以便于将其包裹成Json格式传递。但在判断时,会将其转换成MessageType格式的枚举型,MessageType暂定包含以下几种状态,

public enum MessageType
{
    /// <summary>
    /// 新用户进入
    /// <summary>
    NewUserIn = 1,

    /// <summary>
    /// 用户离开
    /// <summary>
    UserExit = 2,

    /// <summary>
    /// 新用户提供自身信息
    /// <summary>
    ReprotUserInfo = 3,

    /// <summary>
    /// 新文字消息
    /// </summary>
    NewTextMessage = 4,

    /// <summary>
    /// 广播基本信息
    /// </summary>
    BroadcastBasicInfo = 5
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

二、项目结构

1、服务端项目结构如下: 
ChatRoom项目结构视图

这是一个windows控制台程序,Program.cs是入口,ChatWebSocket.cs是核心代码快,外部引用的dll包括Json操作类库和Socket操作类库。 
其中,Model文件夹下的是上面提到的一些基本数据结构,这里看一下核心的代码ChatWebSocket。这当中用到的SuperSocket已经将socket的主要操作封装的很完备了,使用方法如下:

/* 侦听地址(注意,此处的地址一定要和前端js中的地址一致!!) */
const string IP = "127.0.0.1";
/* 侦听端口 */
const int PORT = 2016;

/* SuperWebSocket中的WebSocketServer对象 */
WebSocketServer wsServer = null;

/* 当前在线用户列表 */
List<OnlineUser> olUserList = new List<OnlineUser>();

/* 定时通知客户端线程 */
BackgroundWorker bkWork = null;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这里,WebSocketServer是SuperSocket中封装好的Socket服务端类,olUserList 是在线用户列表,bkWork 是后台线程,负责定时向客户端发送一些系统信息。

构造函数中:

public ChatWebSocket()
{
    /* 初始化 以及 相关事件注册 */
    wsServer = new WebSocketServer();

    /* 有新会话握手并连接成功 */
    wsServer.NewSessionConnected += WsServer_NewSessionConnected;
    /* 有会话被关闭 可能是服务端关闭 也可能是客户端关闭 */
    wsServer.SessionClosed += WsServer_SessionClosed;
    /* 有新文本消息被接收 */
    wsServer.NewMessageReceived += WsServer_NewMessageReceived;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

WebSocketServer有几个比较重要的事件, 
1、NewSessionConnected 有新会话握手并连接成功 
2、SessionClosed 有会话被关闭 可能是服务端关闭 也可能是客户端关闭 
3、NewMessageReceived 有新文本消息被接收 
其中NewMessageReceived事件是消息传递的重要事件,也是我们重点处理的事件,下面将3个事件的方法体列出:

// 有新会话握手并连接成功
private void WsServer_NewSessionConnected(WebSocketSession session)
{
    LogHelper.Write(session.SessionID.ToString() + " Connect!");
}

// 有新文本消息被接收
private void WsServer_NewMessageReceived(WebSocketSession session, string value)
{
    LogHelper.Write("Receive Message:" + value);

    var msg = JsonConvert.DeserializeObject<Message>(value);
    MessageType mt = (MessageType)msg.Type;
    switch (mt)
    {
        /* 用户报告自己信息,将UserId与SessionId关联 */
        case MessageType.ReprotUserInfo:
            olUserList.Add(new OnlineUser
            {
                SessionId = session.SessionID, 
                Id = msg.FromUserId, 
                RealName = msg.FromUserName, 
                RoomId = msg.RoomId 
            });
            /* 通知其他用户 */
            SendMessage(session, new Message
            {
                FromUserId = msg.FromUserId,
                FromUserName = msg.FromUserName,
                ToUserId = 0,// 同一房间的人
                Type = (int)MessageType.NewUserIn,
                Content = "",
                Time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
                RoomId = msg.RoomId
            });
            break;

        /* 用户文字(图片)消息,服务器进行转发 */
        case MessageType.NewTextMessage:
            /* 通知其他用户 */
            msg.Time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
            SendMessage(session, msg);
            break;

        default: break;
    }
}

// 有会话被关闭 可能是服务端关闭 也可能是客户端关闭
private void WsServer_SessionClosed(WebSocketSession session, CloseReason value)
{
    LogHelper.Write(session.SessionID.ToString() + " Exit!");

    var u = olUserList.Find(m => m.SessionId == session.SessionID);
    if (u == null)
    {
        return;
    }
    olUserList.Remove(u);

    // 通知其他用户
    SendMessage(session, new Message
    {
        FromUserId = u.Id,
        FromUserName = u.RealName,
        ToUserId = 0,// 同一房间的人
        Type = (int)MessageType.UserExit,
        Content = "",
        RoomId = u.RoomId
    });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

其中,SendMessage(WebSocketSession session, Message msg)方法如下:

private void SendMessage(WebSocketSession session, Message msg)
{
    // -1:全体;0:同一房间;剩下:特定的用户
    var users = msg.ToUserId == -1 ? olUserList : msg.ToUserId == 0 ? olUserList.Where(m => m.RoomId == msg.RoomId).ToList() : olUserList.Where(m => m.Id == msg.ToUserId).ToList();
    users.ForEach(u =>
    {
        var ss = session.AppServer.GetAppSessionByID(u.SessionId);
        if (ss != null)
        {
            ss.Send(JsonConvert.SerializeObject(msg));
        }
     });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

另外,WebSocketServer的启动和停止也非常的简单:

public void Start()
{
    if (!wsServer.Setup(IP, PORT))
    {
        throw new Exception("设置WebSocket服务侦听地址失败!");
    }

    if (!wsServer.Start())
    {
        throw new Exception("启动WebSocket服务侦听失败!");
    }
}

public void Stop()
{
    if (wsServer != null)
    {
        wsServer.Stop();
    }
}
posted @ 2017-08-03 11:31  从未被超越  阅读(982)  评论(0编辑  收藏  举报