上学期的作业~呵呵O(∩_∩)O~
貌似图片贴不上来~额。。
实验要求:
使用远程控制框架实现一个聊天室程序的设计。
要求:
1、聊天室客户端采用Windows平台;
2、服务器可以使用控制面板应用程序,有能力的同学可以使用Windows平台完成;
3、能够在客户端登录服务器并正确显示当前在线客户列表,并能正常发送发言数据。
代码及其注释:
解决方案由四个项目组成,分别是客户端、服务器端、远程对象动态库、远程对象接口动态库,生成的对象分别为chat_c.exe , chat_s.exe , chat_d.dll , chat_cd.dll ,所有项目均引用了System.Runtime.Remoting.dll
以下是chat_d.dll的源代码:
using System;
namespace chat_d
{
public class remoting : MarshalByRefObject
{
public ulong 用户版本号;//用于记录用户列表的新旧
public ulong 聊天版本号;//用于记录聊天记录的新旧
public ulong 私聊版本号;//用于记录私聊记录的新旧
public System.Collections.ArrayList userList;//用于存储用户列表
public System.Collections.ArrayList talkList;//用于存储公共聊天记录
public System.Collections.ArrayList secretList;//用于存储私聊记录
public remoting ( )//构造函数,初始化变量
{
userList = new System . Collections . ArrayList ( );
talkList = new System . Collections . ArrayList ( );
secretList = new System . Collections . ArrayList ( );
用户版本号 = 0;
聊天版本号 = 0;
私聊版本号 = 0;
}
public bool 同步用户版本号 ( ref ulong u )
{
//判断客户端的用户记录是否为最新
//如果不是最新的则返回true
//并同步用户版本号
if ( u != 用户版本号 )
{
u = 用户版本号;
return true;
}
return false;
}
public bool 同步聊天版本号 ( ref ulong t )
{
//判断客户端的聊天记录是否为最新
//如果不是最新的则返回true
//并同步聊天版本号
if ( t != 聊天版本号 )
{
t = 聊天版本号;
return true;
}
return false;
}
public bool 同步私聊版本号 ( ref ulong s )
{
//判断客户端的私聊记录是否为最新
//如果不是最新的则返回true
//并同步私聊版本号
if ( s != 私聊版本号 )
{
s = 私聊版本号;
return true;
}
return false;
}
public bool 用户被踢出 ( string n )
{
//判断某用户是否被踢出
//如果被踢出则返回true
if ( !userList . Contains ( n ) )
return true;
return false;
}
public bool addUser ( string newUser , ref System . Collections . ArrayList list )
{
//首先判断用户名是否存在
//如果存在则返回false
bool isThere = userList . Contains ( newUser );
if ( isThere )
return false;
else
{
//增加用户并更新用户版本号
//同步客户端的用户列表
userList . Add ( newUser );
用户版本号++;
list = userList;
return true;
}
}
public void quitUser ( string user )
{
//将用户从聊天列表中删除
userList . Remove ( user );
用户版本号++;
}
public void 同步用户列表 ( ref System . Collections . ArrayList ul)
{
ul = userList;
}
public void 同步聊天记录 ( ref System . Collections . ArrayList tl )
{
tl = talkList;
}
public void chat ( string user , string message )
{
//格式化客户端发出的公共信息
string str = user + " " + DateTime . Now . ToString ( "T" );
//并添加到聊天列表
talkList . Add ( str );
talkList . Add ( " " + message );
//更新聊天版本号
聊天版本号++;
}
public void chat ( string user , string collecter , string message )
{
//格式化客户端发出的私人聊天信息
string 抬头 = user + " 悄悄对 " + collecter + "说" + " " + DateTime . Now . ToString ( "T" );
//由于自定义类在remoting的传输中涉及串行化的问题
//string类型的数组拆箱比较麻烦
//这里采用每4个字符串一组的形式存储私聊信息
//如果私聊用户不存在
//则存入用户不在的信息
if ( 用户被踢出 ( collecter ) )
{
secretList . Add ( user );
secretList . Add ( collecter );
secretList . Add ( 抬头 );
secretList . Add ( "可惜" + collecter + "不在哦!" );
}
else
{
//将私聊信息存入私聊列表
string 对话 = " " + message;
secretList . Add ( user );
secretList . Add ( collecter );
secretList . Add ( 抬头 );
secretList . Add ( 对话 );
}
//更新私聊版本号
私聊版本号++;
}
public void 同步私聊 ( ref System . Collections . ArrayList sl )
{
//同步私聊列表
sl = secretList;
}
public void 同步私聊 ( string name , ref System . Collections . ArrayList sl )
{
for ( int i = 0 ; i < secretList.Count ; i += 4 )
{
//判断该远程用户是否为私聊双方中的一方
//如果是,则把跟该两人相关的私聊记录加入到
//该远程用户的私聊列表
if ( secretList [ i ] . ToString ( ) == name || secretList [ i + 1 ] . ToString ( ) == name )
{
sl . Add ( secretList [ i ] . ToString ( ) );
sl . Add ( secretList [ i + 1 ] . ToString ( ) );
sl . Add ( secretList [ i + 2 ] . ToString ( ) );
sl . Add ( secretList [ i + 3 ] . ToString ( ) );
}
}
}
}
}
以下是chat_cd.dll的源代码:
using System;
namespace chat_d
{
public interface remoting //用于欺骗编译器的接口
{
bool 同步用户版本号 ( ref ulong u );
bool 同步聊天版本号 ( ref ulong t );
bool 同步私聊版本号 ( ref ulong s );
bool 用户被踢出 ( string n );
bool addUser ( string newUser , ref System . Collections . ArrayList list );
void quitUser ( string user );
void 同步用户列表 ( ref System . Collections . ArrayList ul);
void 同步聊天记录 ( ref System . Collections . ArrayList tl );
void chat ( string user , string message );
void chat ( string user , string collecter , string message );
void 同步私聊 ( ref System . Collections . ArrayList sl );
void 同步私聊 ( string name , ref System . Collections . ArrayList sl );
}
}
以下是chat_s.exe的界面设计:
chat_s.exe的主要代码:
using System;
using System . Collections . Generic;
using System . ComponentModel;
using System . Data;
using System . Drawing;
using System . Text;
using System . Windows . Forms;
namespace chat_s
{
public partial class 服务端 : Form
{
//服务端的列表版本号
public ulong 用户版本号;
public ulong 聊天版本号;
public ulong 私聊版本号;
//服务端的列表
public System.Collections.ArrayList userList;
public System.Collections.ArrayList talkList;
public System.Collections.ArrayList secretList;
//tcp信道
public System.Runtime .Remoting.Channels.Tcp.TcpServerChannel 服务器;
//远程对象
chat_d.remoting 远程对象;
//在用户列表被选中的用户的用户名
string 选中的用户;
public 服务端 ( )//构造函数,初始化各变量
{
try
{
服务器 = new System . Runtime . Remoting . Channels . Tcp . TcpServerChannel ( 1990 );
userList = new System . Collections . ArrayList ( );
talkList = new System . Collections . ArrayList ( );
secretList = new System . Collections . ArrayList ( );
远程对象 = new chat_d . remoting ( );
用户版本号 = 0;
聊天版本号 = 0;
选中的用户 = null;
InitializeComponent ( );
}
catch ( System . Net . Sockets . SocketException )//端口已被占用?
{
MessageBox . Show ( "同时启动了两个服务端实例或者端口已被其他程序占用!”);
this . Close ( );
}
}
private void 开关_Click ( object sender , EventArgs e )
{//当点击开关键时启动或者停止客户端
if ( 开关 . Text == "启动" )
{
启动 ( );
}
else
{
停止 ( );
}
}
private void 计时器_Tick ( object sender , EventArgs e )
{
//判断用户列表是否为最新
//如果不是最新,则函数自动同步版本号
if ( 远程对象 . 同步用户版本号 ( ref 用户版本号 ) )
{
//同步本地用户列表
远程对象 . 同步用户列表 ( ref userList );
//建立一个本地用户的枚举容器
System.Collections.IEnumerator tt=userList . GetEnumerator ( );
//清空本地用户列表
用户列表 . Items . Clear ( );
//更新本地用户列表
while ( tt . MoveNext ( ) )
{
用户列表 . Items . Add ( tt . Current );
}
}
//刷新聊天信息
if ( 远程对象 . 同步聊天版本号 ( ref 聊天版本号 ) )
{
远程对象 . 同步聊天记录 ( ref talkList );
System.Collections.IEnumerator tt1=talkList . GetEnumerator ( );
while ( tt1 . MoveNext ( ) )
{
if ( !聊天窗口 . Text . Contains ( tt1 . Current . ToString ( ) ) )
{
聊天窗口 . Text = 聊天窗口 . Text + tt1 . Current + "/r/n";
}
}
}
//刷新私人聊天信息
//判断私聊信息是否为最新
//如果不是最新,则函数自动同步版本号
if ( 远程对象 . 同步私聊版本号 ( ref 私聊版本号 ) )
{
//同步所有私人聊天记录
远程对象 . 同步私聊 ( ref secretList );
//写入本地的私聊窗口
for ( int i = 0 ; i < secretList . Count ; i += 4 )
{
if ( !私聊框 . Text . Contains ( secretList [ i + 2 ] . ToString ( ) ) )
{
私聊框 . Text += secretList [ i + 2 ] . ToString ( ) + "/r/n";
私聊框 . Text += secretList [ i + 3 ] . ToString ( ) + "/r/n";
}
}
}
}
private void 踢出_Click ( object sender , EventArgs e )
{
//将远程对象中用户列表中的用户删除
//配合客户端的设置可以实现踢出用户
远程对象 . quitUser ( 选中的用户 );
}
private void 用户列表_MouseClick ( object sender , MouseEventArgs e )
{
//将用户列表被选中的用户的姓名赋给选中的用户
选中的用户 = 用户列表 . Text;
}
void 启动 ( )
{
开关 . Text = "停止";
//用Marshal注册一个长期存在的远程对象
System.Runtime.Remoting.ObjRef objref = System . Runtime . Remoting . RemotingServices . Marshal ( 远程对象 , "chat" );
计时器 . Enabled = true;
获取本机IP ( );
}
void 停止 ( )
{
开关 . Text = "启动";
计时器 . Enabled = false;
this . Text = "服务端 - 未启动";
用户列表 . Items . Clear ( );
聊天窗口 . Clear ( );
私聊框 . Clear ( );
//断开长期存在的远程对象
System . Runtime . Remoting . RemotingServices . Disconnect (远程对象);
}
void 获取本机IP ( )
{
//本地主机地址类
System.Net. IPHostEntry myHost = new System . Net . IPHostEntry ( );
try
{
// Dns.GetHostName()获取本地计算机的主机名
// Dns.GetHostByName()获取指定 DNS 主机名的 DNS 信息
//得到本地主机的DNS信息
myHost = System . Net . Dns . GetHostByName ( System.Net.Dns . GetHostName ( ) );
//显示本地主机的IP地址表
this.Text = "服务端 - 本机的IP地址是"+myHost . AddressList [ 0 ] . ToString ( );
}
catch ( Exception error )//如果发生Exception错误则提示出错
{
MessageBox . Show ( error . Message );
}
}
}
}
chat_c.exe的设计说明
chat_c包含三个窗口类,分别是聊天框、登陆框、获取IP框,其作用分别为用于实现聊天功能、用于实现用户登陆、用户设置IP。
登陆框的设计视图
登陆框的主要代码
using System;
using System . Collections . Generic;
using System . ComponentModel;
using System . Data;
using System . Drawing;
using System . Text;
using System . Windows . Forms;
namespace chat
{
public partial class 登录框 : Form
{ //TCP信道
public System.Runtime.Remoting.Channels.Tcp .TcpChannel 客户端;
public chat_d.remoting 远程对象;
//用户列表
public System.Collections.ArrayList userList;
//本地用户名
public string name;
//服务器端IP,初始化为127.0.0.1本地
public string IP;
//"获取IP"的窗口对象
获取IP 获取IP;
public bool 登录成功;
public bool 存在重名;
public 登录框( )//构造函数,初始化对象。
{
name = "默认姓名";
登录成功= false;
存在重名= false;
IP = "127.0.0.1";
获取IP = new 获取IP ( );
userList = new System . Collections . ArrayList ( );
InitializeComponent ( );
}
private void button1_Click ( object sender , EventArgs e )
{
启动( );
}
private void button2_Click ( object sender , EventArgs e )
{
存在重名= false;//结合主函数防止在登录框关闭后聊天框被打开
登录成功= false;
this . Close ( );
}
private void textBox1_KeyPress ( object sender , KeyPressEventArgs e )
{ //当在文字框按回车时触发
if ( e . KeyChar . ToString ( ) == "/r" )
{
启动( );
}
}
//启动客户端,注册远程对象的函数
void 启动( )
{
try
{
if ( 远程对象== null )
{
客户端= new System . Runtime . Remoting . Channels . Tcp . TcpChannel ( ); //实例化信道
System . Runtime . Remoting . Channels . ChannelServices . RegisterChannel ( 客户端, false ); //注册信道
远程对象= ( chat_d . remoting ) Activator . GetObject ( typeof ( chat_d . remoting ) , "tcp://"+IP+":1990/chat" ); //注册远程对象
}
//获取本地用户名
name = textBox1 . Text;
//向远程对象中添加用户
//如果存在重名则添加失败且函数返回false.
存在重名= !远程对象. addUser ( name , ref userList );
if ( 存在重名)
MessageBox . Show ( "网络存在重名,请重新登录" );
else
{
登录成功= true;//连接失败等情况由catch处理,所以如果没出错的话登录成功都是true
this . Close ( );
}
}
catch ( System . Net . WebException )//网络连接出错
{
MessageBox . Show ( "连接失败,请检查网络连接" );
登录成功= false;
System . Runtime . Remoting . Channels . ChannelServices . UnregisterChannel ( 客户端);
}
catch ( System . Net . Sockets . SocketException )//无法注册对象时被捕获
{
MessageBox . Show ( "连接失败,请检查网络连接" );
登录成功= false;
System . Runtime . Remoting . Channels . ChannelServices . UnregisterChannel ( 客户端);
}
catch ( System . Runtime . Remoting . RemotingException )//信道协议冲突或者服务端未启动时被捕获
{
try//如果是由于信道冲突引起则不会影响正常使用
{
远程对象.用户被踢出(name );//测试远程对象是否正常,如果不正常则抛出错误
this . Close ( );
}
catch ( System . Runtime . Remoting . RemotingException )//若是服务端未启动
{
MessageBox . Show ( "连接失败,请检查网络连接" );
登录成功= false;
}
}
}
private void 修改IP_Click ( object sender , EventArgs e )
{
//触发修改IP的窗口
获取IP . ShowDialog ( );//显示修改IP的窗口
//将获得的服务端IP地址赋给变量
IP = 获取IP . IP;
}
}
}
获取IP框的设计图
获取IP的主要代码
using System;
using System . Collections . Generic;
using System . ComponentModel;
using System . Data;
using System . Drawing;
using System . Text;
using System . Windows . Forms;
namespace chat
{
public partial class 获取IP : Form
{
public string IP;//定义一个字符串用户储存IP
public 获取IP ( )
{
InitializeComponent ( );
}
private void button1_Click ( object sender , EventArgs e )
{
//将对话框的IP存入变量后关闭窗口
IP = maskedTextBox1 . Text;
this . Hide ( );
}
}
}
聊天框的设计视图
聊天框的主要代码
using System;
using System . Collections . Generic;
using System . ComponentModel;
using System . Data;
using System . Drawing;
using System . Text;
using System . Windows . Forms;
namespace chat
{
public partial class 聊天框 : Form
{
//信道,用于接受登录框的信道
public System.Runtime.Remoting.Channels.Tcp.TcpChannel 客户端;
//本地用户、聊天、私聊列表
public System.Collections.ArrayList userList;
public System.Collections.ArrayList talkList;
public System.Collections.ArrayList secretList;
//本地的用户、聊天、私聊版本号
public ulong 用户版本号;
public ulong 聊天版本号;
public ulong 私聊版本号;
//远程对象,用户接受登录框传递的远程对象
chat_d.remoting 远程对象;
//本地用户名
public string name;
public 聊天框 ( ref 登录框 登录 )//构造函数
{
//初始化列表
userList = new System . Collections . ArrayList ( );
talkList = new System . Collections . ArrayList ( );
secretList = new System . Collections . ArrayList ( );
//接受登录框传递的部分对象
远程对象 = 登录 . 远程对象;
客户端 = 登录 . 客户端;
name = 登录 . name;
userList = 登录 . userList;
//出事后聊天版本号
用户版本号 = 0;
聊天版本号 = 0;
InitializeComponent ( );
//显示欢迎标题栏
this . Text = "PP — 欢迎您 " + name;
}
private void 聊天框_FormClosing ( object sender , FormClosingEventArgs e )
{
try
{ //正常退出时将远程对象用户列表中的自己除名
远程对象 . quitUser ( name );
System . Runtime . Remoting . Channels . ChannelServices . UnregisterChannel ( 客户端 );
}
catch ( System . Net . Sockets . SocketException )
{
//由于服务端关闭连接引起的关闭,不做任何动作
//此时无法访问远程对象
}
catch ( System . Runtime . Remoting . RemotingException )
{
//由于远程对象断开连接引起的关闭,不做任何动作
//此时无法访问远程对象
}
}
private void 发送_Click ( object sender , EventArgs e )
{
公聊 ( );
}
private void 关闭_Click ( object sender , EventArgs e )
{
this . Close ( );
}
private void 用户列表_MouseClick ( object sender , MouseEventArgs e )
{
用户名文字框 . Text = 用户列表 . Text;
}
private void 发送密语_Click ( object sender , EventArgs e )
{
私聊 ( );
}
private void 计时器_Tick ( object sender , EventArgs e )
{
try
{
//刷新客户端的用户列表
//当且仅当远程对象中的用户列表有更新时
//才对本地列表进行更新
if ( 远程对象 . 同步用户版本号 ( ref 用户版本号 ) )
{
远程对象 . 同步用户列表 ( ref userList );
//创建枚举
System.Collections.IEnumerator tt=userList . GetEnumerator ( );
用户列表 . Items . Clear ( );
while ( tt . MoveNext ( ) )
{
用户列表 . Items . Add ( tt . Current );
}
}
//刷新客户端的聊天信息
//当且仅当远程对象中的公共聊天有更新时
//才对本地列表进行更新
if ( 远程对象 . 同步聊天版本号 ( ref 聊天版本号 ) )
{
远程对象 . 同步聊天记录 ( ref talkList );
System.Collections.IEnumerator tt1=talkList . GetEnumerator ( );
//对使用回车发送的文字框进行清空
if ( 对话框 . Text == "/r/n" )
对话框 . Text = "";
while ( tt1 . MoveNext ( ) )
{
//仅对本地聊天窗口添加新的内容
if ( !聊天窗口 . Text . Contains ( tt1 . Current . ToString ( ) ) )
{
聊天窗口 . Text = 聊天窗口 . Text + tt1 . Current + "/r/n";
}
}
}
//判断用户是否被踢出
if ( 远程对象 . 用户被踢出 ( name ) )
{
计时器 . Enabled = false;
MessageBox . Show ( "你被邪恶的管理员踢出聊天室了" );
this . Close ( );
}
//判断是否有私聊
if ( 远程对象 . 同步私聊版本号 ( ref 私聊版本号 ) )
{
//本函数仅会将与指定用户名有关的私聊记录同步到本地
远程对象 . 同步私聊 ( name , ref secretList );
//对使用回车发送的文字框进行清空
if ( 私聊对话框 . Text == "/r/n" )
私聊对话框 . Text = "";
//向窗口添加私聊内容
for ( int i = 0 ; i < secretList . Count ; i += 4 )
{
if ( !聊天窗口 . Text . Contains ( secretList [ i + 2 ] . ToString ( ) ) )
{
聊天窗口 . Text += secretList [ i + 2 ] . ToString ( ) + "/r/n";
聊天窗口 . Text += secretList [ i + 3 ] . ToString ( ) + "/r/n";
}
}
}
}
catch ( System . Net . Sockets . SocketException )//服务端被关闭时的处理
{
计时器 . Enabled = false;
System . Runtime . Remoting . Channels . ChannelServices . UnregisterChannel ( 客户端 );
MessageBox . Show ( "由于服务器断开连接所以程序将关闭" );
this . Close ( );
}
catch ( System . Runtime . Remoting . RemotingException )//远程对象被断开时的处理
{
计时器 . Enabled = false;
System . Runtime . Remoting . Channels . ChannelServices . UnregisterChannel ( 客户端 );
MessageBox . Show ( "由于服务器断开连接所以程序将关闭" );
this . Close ( );
}
}
private void 对话框_KeyPress ( object sender , KeyPressEventArgs e )
{ //响应回车
if ( e . KeyChar . ToString ( ) == "/r" )
{
公聊 ( );
}
}
private void 私聊对话框_KeyPress ( object sender , KeyPressEventArgs e )
{ //响应回车
if ( e . KeyChar.ToString() == "/r" )
{
私聊 ( );
}
}
void 公聊 ( )
{ //调用远程对象实现公聊
远程对象 . chat ( name , 对话框 . Text );
对话框 . Text = "";
}
void 私聊 ( )
{ //不允许对自己进行私聊
if ( 用户名文字框 . Text != name )
{
//实现私聊
远程对象 . chat ( name , 用户名文字框 . Text , 私聊对话框 . Text );
私聊对话框 . Text = "";
}
else
MessageBox . Show ( "用得着跟自己说话么?" );
}
}
}
chat_c.exe的主函数
using System;
using System . Collections . Generic;
using System . Windows . Forms;
namespace chat
{
static class chat_c
{
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main ( )
{
Application . EnableVisualStyles ( );
Application . SetCompatibleTextRenderingDefault ( false );
登录框 登录 = new 登录框 ();
Application . Run ( 登录 );
//当且仅当登陆完全成功时,才启动聊天窗口
if ( !登录 . 存在重名 && 登录 . 登录成功 )
{
聊天框 聊天 = new 聊天框 ( ref 登录 );
Application . Run ( 聊天 );
}
}
}
}
结果
服务器端运行的情况
初始化
| 启动后
|
当用户登陆并进行了公聊、私聊后
当对dd使用踢出后
|
|
客户端运行的情况
登陆窗口初始化
| 获取IP窗口初始化
|
公共聊天
|
|
进行私聊的时候
|
|
当客户端被关闭后
| 当服务端先于客户端被关闭或者被停止时
|
实验心得
这是我第一次完整的使用VS08创建windows窗体类型的程序,也是第一次进行涉及网络通讯的程序设计。
我觉得整个设计过程中最难理解的部分应该是关于“信道”与“远程对象”之间的使用吧,不过我感到最辛苦的时候是在涉及远程对象的事件订阅的时。当时因为不希望让客户端或者服务器通过不断的刷新来实现聊天记录的更新,而是真正有一次更新才刷新一次,所以想到通过使所有客户端都订阅同一个远程对象的事件实现公共聊天,并通过订阅一个带有识别参数的远程对象事件的方式来实现私聊。不过这东西虽然理论上看起来可行,但由于remoting本身设计的缘故,使用起来一点都不简单,我翻阅了很多资料,写了好久,却最终发现即便可以实现客户端订阅远程对象的事件以及服务器端订阅远程对象的事件,却无法有订阅者去主动触发自己订阅的事件,会使得程序陷入死循环。
在研究了很久以后,我最终选择放弃了使用事件驱动。这个选择本身比研究它更难过,因为我已经花了十几个小时在它上面了。虽然结果不是很让人满意,不过这个过程中还是学到了很多东西,也了解了很多关于网络通讯的知识。
另外就是对于windows窗体编程,有了更多的了解。面对种类繁多的控件属性以及为了实现某些效果或者因为不理解一些事件和属性的作用,不断上网查资料或者查MSDN或者进行不断的尝试,这都对我有很多帮助。