PPMM房间服务协议设计及实现
(任何引用请注明:转载于美人山下http://www.beautyhill.blogspot.com)
    房间服务就是维护一个房间列表,同步服务器端和客户端之间的用户数据。在较低层的tcp协议之上,笔者创建了一个协议类,他的职责就是约束服务器与客户端之间的“通信方言”:
using System;
using System.Collections.Generic;
using System.Text;
namespace Server {
public class Protocol {
private Protocol(){
}
public const char SplitTag = '\n';
public enum ServerCommand {
LoginOK = 0,//登陆成功
RoomFriendNew,//增加室友
RoomFriendDelete,//删除室友
RoomNew,//增加房间
RoomDelete,//删除房间
RoomChange,//更改房间成功
RoomChannel,//播放频道
UserSearch//查询用户结果
};
public enum ClientCommand {
Regist = 0,//注册
Login,//登陆
RoomCreate,//创建房间
RoomIn,//进入房间
RoomOut,//走出房间
PersonalMessage,//私聊
RoomChannel,//播放频道
UserSearch,//查询用户
SystemOut//注销
}
}
}
    服务器和客户端都是通过这个类查找相应的命令进行编码发送命令的动作,同时也通过这个类将接受到的命令进行解码操作。
    原理其实真的不难。但值得注意的是:将协议用专门的类进行约束,这是一项进步,他十分有效的降低了以往手工命令编码的高错误率。同时,在命令的传输的时候,不是命令字符串本身,而是某一个数字(这是enum枚举类的特性);这十分有效的节省了带宽。
    对服务器和客户端而言,分清各自的命令十分重要。笔者的亲身感受是,在没有创建Protocol以前,双方的命令对于编程人员是混淆的。即,没有SeverCommand和ClientCommand的区分,用了同一套命令,这导致了交互的混乱。当笔者尝试了两套命令后,这种优良结构带来的精神和生理上的清爽痛快,大概只有笔者自己能够体会到了!
    服务器端接受客户端的ClientCommand,并从Protocol处进行解析;而同时从Protocol处编码ServerCommand,并向客户端发送。以下代码为服务器端解析命令的过程:
switch (int.Parse(items[0])) {
case (int)(Protocol.ClientCommand.Regist):
this.regist(items[1], items[2],bool.Parse(items[3]),items[4], a);
break;
case (int)(Protocol.ClientCommand.Login):
this.login(items[1],items[2], a);
break;
case (int)(Protocol.ClientCommand.RoomCreate):
this.roomCreate(items[1], a);
break;
case (int)(Protocol.ClientCommand.RoomIn):
this.roomChange(items[1], a);
break;
case (int)(Protocol.ClientCommand.RoomOut):
this.roomChange(this.tbLobbyName.Text, a);
break;
case (int)(Protocol.ClientCommand.SystemOut):
this.Disconnect(a);
break;
case (int)(Protocol.ClientCommand.PersonalMessage):
string s = "[私语] " + a.Name + " - " + items[1] + ": " + items[2];
(this.assistants[items[1]] as Assistant).Send(s);
a.Send(s);
foreach (Assistant ass in a.Watchers)
ass.Send(s);
break;
case (int)(Protocol.ClientCommand.UserSearch):
this.userSearch(items[1], a);
break;
case (int)(Protocol.ClientCommand.RoomChannel):
if (a.IsHost) {
foreach (Assistant rm in a.Room.Users) {
rm.Send(Protocol.SplitTag.ToString() + (int)(Protocol.ServerCommand.RoomChannel)
+ Protocol.SplitTag.ToString() + items[1]);
}
(this.rooms[a.Room.Name] as Room).ChannelID = items[1];
foreach (Assistant ass in a.Watchers) {
ass.Send(a.Name + " 在其房间播放了节目 " + items[1]);
}
}
else
a.Send("你不是房主,无法点播!");
break;
default:
a.Send("无效操作!");
break;
}
    客户端接受服务器端的ServerCommand,并从Protocol处进行解析;而同时从Protocol处编码ClientCommand,并向服务器端发送。以下代码为客户端解析命令的过程:
switch (int.Parse(items[0])) {
case (int)(Protocol.ServerCommand.LoginOK):
for (int i = 1; i < items.Length; i++) {
this.tvRoomsNodesAdd(items[i]);
}
this.rtbMessageAppendText("\n登陆成功!");
this.gbLoginEnabled(false);
this.hasLogin = true;
this.tcChatSelectedIndex(1);
this.lbNameText("[" + this.UserName + "] ");
this.formText(" - " + this.UserName);
this.tcChannelsSelectedIndex(0);
this.SaveUserNames();
this.tcMenusSelectedIndex(0);
break;
case (int)(Protocol.ServerCommand.RoomFriendNew):
this.rtbMessageAppendText("\n" + items[1] + " 进入了 " + items[2]);
this.lvRoomFriendsItemAdd(items[1]);
break;
case (int)(Protocol.ServerCommand.RoomFriendDelete):
this.rtbMessageAppendText("\n" + items[1] + " 离开了 " + items[2]);
this.lvRoomFriendsItemRemove(items[1]);
break;
case (int)(Protocol.ServerCommand.RoomNew):
this.tvRoomsNodesAdd(items[1]);
break;
case (int)(Protocol.ServerCommand.RoomDelete):
this.rtbMessageAppendText("\n" + items[1] + " 被撤销!");
this.tvRoomsNodesRemove(items[1]);
break;
case (int)(Protocol.ServerCommand.RoomChange):
this.rtbMessageAppendText("\n我步入了 " + items[1]);
this.lvRoomFriendsClear();
for (int i = 3; i < items.Length; i++) {
this.lvRoomFriendsItemAdd(items[i]);
}
this.tpRoomChatText(items[1]);
if (MessageBox.Show("是否加载房间的频道?", "提示",
MessageBoxButtons.OKCancel, MessageBoxIcon.Question) == DialogResult.OK)
this.Play(items[2]);
break;
case (int)(Protocol.ServerCommand.RoomChannel):
this.rtbMessageAppendText("\n房主开始了节目播放 ...");
if (MessageBox.Show("是否加载房间的频道?", "提示",
MessageBoxButtons.OKCancel, MessageBoxIcon.Question) == DialogResult.OK)
this.Play(items[1]);
break;
case (int)(Protocol.ServerCommand.UserSearch):
this.lvSearchResultClear();
this.gbSearchResultText("好友搜索");
System.Collections.Generic.List
ls.Add("名称");
ls.Add("性别");
ls.Add("描述");
this.lvSearchResultColumns(ls);
for (int i = 1; i < items.Length; i += 3) {
ls.Clear();
ls.Add(items[i]);
ls.Add(items[i + 1]);
ls.Add(items[i + 2]);
this.lvSearchResultItemAdd(ls);
}
this.tcMenusSelectedIndex(2);
break;
default:
this.rtbMessageAppendText("\n服务器消息错误: " + responseData);
break;
}
    至于命令的编码与发送,这与上面的解码一样,是个极其简单的过程,笔者不在这里赘述。有兴趣的初学者可以联系我寻求更多说明。


没有评论:
发表评论