可行性分析
(1)操作可行性
聊天室是提供给网民一个交友与娱乐的场所。在聊天室里,网民可选择自己的聊天对象,与其进行对话交流,是网民之间相互沟通、交流情感的最佳方式之一。
利用聊天室,用户可以通过网络在线与距离遥远的其他一位或多位用户进行信息交流。既可以一对一,又可以一对多,还可以形成小组进行多对多讨论,方便网民在线私聊与群聊,是现行网络最流行、最被广泛应用的通讯工具。
(2)技术可行性
采用C/S模式,基于Socket编程的方式,使得各个用户通过服务器转发实现聊天的功能;
分为两大模块:客户端模块和服务器端模块;
客户端模块的主要功能
- 登陆功能:用户可以注册,然后选择服务器登入聊天室;
- 显示用户:将在线用户显示在列表中;
- 接收信息:能接收其他用户发出的信息;
- 发送信息:能发出用户要发出的信息;
- 自定义颜色:可以个性化字体颜色;
- 退出功能:用户离线,进行通知;
- 连接设置:显示连接的IP地址和端口号。
服务器端模块的主要功能
- 检验登陆信息:检查登陆信息是否正确,并向客户端返回登陆信息,如信息正确,就允许用户登陆;
- 显示在线状态:将该用户的状态发给各在线用户;
- 转发聊天信息:将消息转发给所有在线的用户;
- 端口服务:输入要侦听的端口,默认为9590;
- 启动服务:启动服务器,并开始在设置的端口中侦听,客户端用户可以登录并开始聊天;
- 停止服务:关闭服务器,侦听结束。客户端用户不能再聊天;
- 服务器IP:输入要监听的IP;
- 退出服务器:退出程序,并停止服务。
(3)经济可行性
成本方面:本系统主要在实验室开发,相应资料已具备,开发成本和运行成本能够满足。
效益方面:人们日常生活中越来越多地使用聊天室这项应用来为自己的工作和学习服务。一个操作简单,界面友好、运行稳定的聊天室,对于小型局域网的用户可以起到很好的交流作用。
设计原理
聊天协议的应答
聊天状态:CLOSED和CONNECTED状态
执行CONN命令后进入CONNECTED状态,执行下列命令:
- CONN:连接聊天室服务器
- JOIN:加入聊天(通知其他用户本人已经加入聊天室服务器)
- LIST:列出所有的用户(向客户端发送全部的登录用户名字)
- CHAT:发送聊天信息(公开的聊天信息)
- PRIV:进行私聊(三个参数:私聊信息用户;接收私聊信息用户;发送信息)
- EXIT:客户端向服务器发送离开请求;
- QUIT:退出聊天,服务器向客户端发送退出命令(执行QUIT命令聊天状态变为CLOSED)。
服务器协议解析
当有客户端连接聊天室服务器后,服务器立刻为这个客户建立一个数据接收的线程(多用户程序必备)。在接收线程中,如果收到聊天命令,就对其进行解析处理,服务器可以处理五种命令:CONN\LIST\CHAT\PRIV\EXIT。
服务器接收到CONN命令,就向其他用户发送JOIN命令告诉有用户加入,然后把当前的全部用户信息返回给刚刚加入的用户,以便在界面上显示用户列表。当接收到EXIT命令后,就清除当前用户的信息,然后向其他用户发送QUIT命令,告诉其他用户退出了,这些用户的客户端把离开的用户从用户列表中删除。
客户端协议解析
当客户端连接到服务器后,服务器立刻建立一个数据接收的独立线程。在接收线程中,如果收到了聊天命令,就对其进行解析处理。客户端一共处理的命令有五种:OK\ERR\LIST\JOIN\QUIT命令。
具体实现
编写实体Bean
编写一个独立的类即Client类,封装了客户端的信息与连接,每一个客户进入聊天室,就创建一个Client对象,用于保存该用户的信息并接收用户数据和发送信息到客户端
Client.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Collections;
namespace Server
{
public class Client
{
private string name;//保存用户名
private Socket currentSocket = null;//保存与当前用户连接的Socket对象
private string ipAddress;//保存用户的IP地址
private FrmServer server;
//保留当前连接的状态:
//closed --> connected --> closed
private string state = "closed";
public Client(FrmServer server, Socket clientSocket)
{
this.server = server;
this.currentSocket = clientSocket;
ipAddress = getRemoteIPAddress();
}
//获得连接客户端的IP地址,并转换成一个字符串
private string getRemoteIPAddress()
{
return ((IPEndPoint)currentSocket.RemoteEndPoint).Address.ToString();
}
//306.客户端线程方法,每连入一个客户端,会启动这个方法
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
public Socket CurrentSocket
{
get
{
return currentSocket;
}
set
{
currentSocket = value;
}
}
//ServiceClient()函数为服务器接收客户数据的线程主体,主要用来接收用户发送来的数据,并处理聊天命令
public void ServiceClient()
{
string[] tokens = null;
byte[] buff = new byte[1024];
bool KeepConnect = true;//用循环来不断的与客户端交互,直到客户端发出EXIT命令
while (FrmServer.ServiceFlag && KeepConnect)
{
tokens = null;
try
{
//如果没有信息达到服务器端,则休息
if (currentSocket == null || currentSocket.Available < 1)
{
Thread.Sleep(200);
continue;
}
//接收数据并存入buff数组中
int len = currentSocket.Receive(buff);
//将字符数组转化为字符串
string clientCommand = System.Text.Encoding.Default.GetString(buff, 0, len);
//tokens[0]中保存了命令标志符(CONN、CHAT、PRIV、LIST或EXIT)
tokens = clientCommand.Split(new Char[] { '|' });
if (tokens == null)
{
Thread.Sleep(150);
continue;
}
}
catch (Exception ex)
{
server.updateUI("发生异常:" + ex.Message);
}
if (tokens[0] == "CONN")
{
//此时接收到的命令格式为:
//命令标志符(CONN)|发送者的用户名|,
//tokens[1]中保存了发送者的用户名
this.name = tokens[1];
if (FrmServer.clients.Contains(this.name))
{
SendToClient(this, "ERR|User " + this.name + " 已经存在");
}
else
{
Hashtable syncClients = Hashtable.Synchronized(
FrmServer.clients);
syncClients.Add(this.name, this);
//更新界面
server.AddUser(this.name);
//对每一个当前在线的用户发送JOIN消息命令和LIST消息命令,
//以此来更新客户端的当前在线用户列表
System.Collections.IEnumerator myEnumerator =
FrmServer.clients.Values.GetEnumerator();
while (myEnumerator.MoveNext())
{
Client client = (Client)myEnumerator.Current;
SendToClient(client, "JOIN|" + tokens[1] + "|");
Thread.Sleep(100);
}
//更新状态
state = "connected";
SendToClient(this, "ok");
//向客户端发送LIST命令,以此更新客户端的当前在线用户列表
string msgUsers = "LIST|" + server.GetUserList();
SendToClient(this, msgUsers);
}
}
else if (tokens[0] == "LIST")
{
if (state == "connnected")
{
//向客户端发送LIST命令,以此更新客户端的当前在线用户列表
string msgUsers = "LIST|" + server.GetUserList();
SendToClient(this, msgUsers);
}
else
{
//send err to server
SendToClient(this, "ERR|state error,Please login first");
}
}
else if (tokens[0] == "CHAT")
{
if (state == "connected")
{
//此时接收到的命令的格式为:
//命令标志符(CHAT)|发送者的用户名:发送内容|
//向所有当前在线的用户转发此信息
System.Collections.IEnumerator myEnumerator =
FrmServer.clients.Values.GetEnumerator();
while (myEnumerator.MoveNext())
{
Client client = (Client)myEnumerator.Current;
//将“发送者的用户名:发送内容”转发给用户
SendToClient(client, tokens[1]);
}
server.updateUI(tokens[1]);
}
else
{
//send err to server
SendToClient(this, "ERR|state error,Please login first");
}
}
else if (tokens[0] == "PRIV")
{
if (state == "connected")
{
//此时接收到的命令格式为:
//命令标志符(PRIV)|发送者用户名|接收者用户名|发送内容|
//tokens[1]中保存了发送者的用户名
string sender = tokens[1];
//tokens[2]中保存了接收者的用户名
string receiver = tokens[2];
//tokens[3]中保存了发送的内容
string content = tokens[3];
string message = sender + " ---> " + receiver + ": " + content;
//仅将信息转发给发送者和接收者
if (FrmServer.clients.Contains(sender))
{
SendToClient(
(Client)FrmServer.clients[sender], message);
}
if (FrmServer.clients.Contains(receiver))
{
SendToClient(
(Client)FrmServer.clients[receiver], message);
}
server.updateUI(message);
}
else
{
//send err to server
SendToClient(this, "ERR|state error,Please login first");
}
}
else if (tokens[0] == "EXIT")
{
//此时接收到的命令的格式为:命令标志符(EXIT)|发送者的用户名
//向所有当前在线的用户发送该用户已离开的信息
if (FrmServer.clients.Contains(tokens[1]))
{
Client client = (Client)FrmServer.clients[tokens[1]];
//将该用户对应的Client对象从clients中删除
Hashtable syncClients = Hashtable.Synchronized(
FrmServer.clients);
syncClients.Remove(client.name);
server.RemoveUser(client.name);
//向客户端发送QUIT命令
string message = "QUIT|" + tokens[1];
System.Collections.IEnumerator myEnumerator =
FrmServer.clients.Values.GetEnumerator();
while (myEnumerator.MoveNext())
{
Client c = (Client)myEnumerator.Current;
SendToClient(c, message);
}
}
//退出当前线程
break;
}
Thread.Sleep(200);
}
}
//SendToClient()方法实现了向客户端发送命令请求的功能
private void SendToClient(Client client, string msg)
{
System.Byte[] message = System.Text.Encoding.Default.GetBytes(msg.ToCharArray());
client.CurrentSocket.Send(message, message.Length, 0);
}
}
}
聊天室服务器端设计
使用Label,TextBox,Button,ListBox,RichTextBox等控件完成服务器端UI
启动服务之前,我们要先对服务端IP和端口号进行设置,设置完成后点击启动服务按钮,对已设置端口进行监听
当服务使用完成后,点击结束服务按钮,停止对端口的监听
界面和功能实现FrmServer.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Collections;
using System.Windows.Forms;
namespace Server
{
public partial class FrmServer : Form
{
internal static Hashtable clients = new Hashtable();//clients数组保存当前在线用户的Client对象
private TcpListener listener;//该服务器默认的监听端口号
static int MaxNum = 100;//服务器可以支持的客户端最大连接数
internal static bool ServiceFlag = false;//开始服务的标志
public FrmServer()
{
InitializeComponent();
CheckForIllegalCrossThreadCalls = false;
}
//服务器监听的端口号通过getValidPort()函数获得
private int getValidPort(string port)
{
int lport;
//测试端口号是否有效
try
{
//是否为空
if (port == "")
{
throw new ArgumentException("端口号为空,不能启动服务器");
}
lport = System.Convert.ToInt32(port);
}
catch (Exception e)
{
Console.WriteLine("无效的端口号:" + e.ToString());
this.rtbMessage.AppendText("无效的端口号:" + e.ToString() + "\n");
return -1;
}
return lport;
}
private void FrmServer_Load(object sender, EventArgs e)
{
string strHostName = Dns.GetHostName();
IPAddress strAddress = Dns.GetHostEntry(Dns.GetHostName()).AddressList[0];
this.txtIPAddress.Text = strAddress.ToString();
}
private void btnStart_Click(object sender, EventArgs e)
{
//确认端口号是有效的,根据TCP协议,范围应该在-65535之间
int port = getValidPort(txtPort.Text);
if (port < 0)
{
return;
}
string ip = txtIPAddress.Text;
try
{
IPAddress ipAdd = IPAddress.Parse(ip);
listener = new TcpListener(ipAdd, port);//创建服务器套接字
listener.Start(); //开始监听服务器端口
this.rtbMessage.Text = "";
this.rtbMessage.AppendText("Socket服务器已经启动!\n正在监听"
+ ip+ "\n端口号:" + this.txtPort.Text + "\n");
//启动一个新的线程,执行方法this.StartSocketListen,
//以便在一个独立的进程中执行确认与客户端Socket连接的操作
FrmServer.ServiceFlag = true;
Thread thread = new Thread(new ThreadStart(this.StartSocketListen));
thread.Start();
this.btnStart.Enabled = false;
this.btnStop.Enabled = true;
}
catch (Exception ex)
{
this.rtbMessage.AppendText(ex.Message.ToString() + "\n");
}
}
//在新的线程中的操作,它主要用于当接收到一个客户端请求时,确认与客户端的链接
//并且立刻启动一个新的线程来处理和该客户端的信息交互
private void StartSocketListen()
{
while (FrmServer.ServiceFlag)
{
try
{
if (listener.Pending())
{
Socket socket = listener.AcceptSocket();
if (clients.Count >= MaxNum)
{
this.rtbMessage.AppendText("已经达到了最大连接数:" + MaxNum + ",拒绝新的连接\n");
socket.Close();
}
else
{
//启动一个新的线程,执行方法this.ServiceClient,处理用户相应的请求
Client client = new Client(this, socket);
Thread clientService = new Thread(new ThreadStart(client.ServiceClient));
clientService.Start();
}
}
//这句话能使系统性能大大提高
Thread.Sleep(200);
}
catch (Exception ex)
{
this.rtbMessage.AppendText(ex.Message.ToString() + "\n");
}
}
}
private void txtPort_TextChanged(object sender, EventArgs e)
{
if (this.txtPort.Text != "")
{
this.btnStart.Enabled = true;
}
}
private void btnStop_Click(object sender, EventArgs e)
{
FrmServer.ServiceFlag = false;
Thread.Sleep(300);
rtbMessage.Text += txtIPAddress.Text + "的服务已经停止!" + "\r\n";
//204. 控制按钮的可用性
this.btnStart.Enabled = true;
this.btnStop.Enabled = false;
}
public void AddUser(string username)
{
this.rtbMessage.AppendText(username + "已经加入\n");//将刚连接的用户加入到当前在向用户列表中
this.userlist.Items.Add(username);
this.usernum.Text = Convert.ToString(clients.Count);
}
public void RemoveUser(string username)
{
this.rtbMessage.AppendText(username + "已经离开\n");//将刚连接的用户加入到当前在向用户列表中
this.userlist.Items.Remove(username);
this.usernum.Text = Convert.ToString(clients.Count);
}
public string GetUserList()
{
string rtn = "";
for (int i = 0; i < userlist.Items.Count; i++)
{
rtn += userlist.Items[i].ToString() + "";
}
return rtn;
}
public void updateUI(string msg)
{
this.rtbMessage.AppendText(msg + "\n");
}
private void FrmServer_FormClosing(object sender, FormClosingEventArgs e)
{
FrmServer.ServiceFlag = false;
}
}
}
聊天室客户端设计
客户端主要完成界面、聊天和传输文件功能的设计,用到以下控件:Label,TextBox,Button,ListBox,RichTextBox,OpenFileDialog,CheckBox。
连接、登陆以前,要对服务器IP、端口号和用户名进行设置, 为调试方便,窗体的Load事件将服务器IP设置为本机IP并对端口6666进行监听,一旦有其他客户端向本客户端发送消息可以选择接收。
接下来就可以进行对话了,在下面的TextBox中输入要发送的信息,点击发送对所有在线人发送信息,若选择私聊CheckBox和在线用户的用户名,则是仅对该用户发送信息,为了体现个性化,系统增加了改变字体颜色的功能。
观察服务器端消息转发情况
界面和功能实现FrmClient.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Client
{
public partial class FrmClient : Form
{
TcpClient tcpClient;//与服务器的连接
private NetworkStream Stream;//与服务器交互的流通道
private static string CLOSED = "closed";
private static string CONNECTED = "connected";
private string state = "closed";
private bool stopFlag;
private Color color;//保存当前客户端显示的颜色
public FrmClient()
{
InitializeComponent();
CheckForIllegalCrossThreadCalls = false;
}
private void btnLogin_Click(object sender, EventArgs e)
{
if (state == CONNECTED)
return;
if (this.username.TextLength == 0)
{
MessageBox.Show(" 请输入你的昵称!", "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
this.username.Focus();//为控件设置焦点
return;
}
try
{
//创建一个客户端套接字,他是Login的一个公共属性
tcpClient = new TcpClient();
tcpClient.Connect(IPAddress.Parse(txtHost.Text), Int32.Parse(txtPort.Text));//向指定的IP地址服务器发出连接请求
Stream = tcpClient.GetStream(); //获得与服务器数据交互的流通道 NetworksStream
//启动一个新的线程,执行方法this.ServerResponse(),以便来响应从服务器发回的信息
Thread thread1 = new Thread(new ThreadStart(this.ServerResponse));
thread1.Start();
//向服务器发送CONN请求命令
//此命令的格式与服务器端的定义的格式一致
//命令格式为:命令标志符CONN|发送者的用户名
string cmd = "CONN|" + this.username.Text + "|";
//将字符串转化为字符数组
Byte[] outbytes = System.Text.Encoding.Default.GetBytes(cmd.ToCharArray());
Stream.Write(outbytes, 0, outbytes.Length);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
private void btnSend_Click(object sender, EventArgs e)
{
try
{
if (!this.cbPrivate.Checked)
{
//此时命令的格式是:命令标识符CHAT|发送者的用户名:发送内容|
string message = "CHAT|" + this.username.Text + ":" + tbSendContent.Text;
tbSendContent.Text = "";
tbSendContent.Focus();
byte[] outbytes = System.Text.Encoding.Default.GetBytes(message.ToCharArray()); //将字符串转化为字符数组
Stream.Write(outbytes, 0, outbytes.Length);
}
else
{
if (lstUsers.SelectedIndex == -1)
{
MessageBox.Show("请在列表中选择一个用户", "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
return;
}
string receiver = lstUsers.SelectedItem.ToString();
//消息的格式是:命令标识符PRIV|发送者的用户名|接收者的用户名|发送内容
string message = "PRIV|{" + this.username.Text + "|" + receiver + "|" + tbSendContent.Text + "|";
tbSendContent.Text = "";
tbSendContent.Focus();
byte[] outbytes = System.Text.Encoding.Default.GetBytes(message.ToCharArray()); //将字符串转化为字符数组
Stream.Write(outbytes, 0, outbytes.Length);
}
}
catch
{
this.rtbMsg.AppendText("网络发生错误!");
}
}
//this.ServerResponse()方法用于接收从服务器发回的信息,根据不同的命令,执行相应的操作
private void ServerResponse()
{
//定义一个byte数组,用于接收从服务器端发来的数据
//每次所能接受的数据包的最大长度为1024个字节
byte[] buff = new byte[1024];
string msg;
int len;
try
{
if (Stream.CanRead == false)
{
return;
}
stopFlag = false;
while (!stopFlag)
{
//从流中得到数据,并存入到buff字符数组中
len = Stream.Read(buff, 0, buff.Length);
if (len < 1)
{
Thread.Sleep(500);
continue;
}
//将字符数组转化为字符串
msg = System.Text.Encoding.Default.GetString(buff, 0, len);
msg.Trim();
string[] tokens = msg.Split(new char[] { '|' });
//tokens[0]中保存了命令标志符LIST JOIN QUIT
if (tokens[0].ToUpper() == "OK")
{
//处理响应
add("命令执行成功!");
}
else if (tokens[0].ToUpper() == "ERR")
{
add("命令执行错误:" + tokens[1]);
}
else if (tokens[0] == "LIST")
{
//此时从服务器返回的消息格式:命令标志符LIST|用户名1|用户名2|。。(所有在线用户名)
//add(“获得用户列表”),更新在线用户列表
lstUsers.Items.Clear();
for (int i = 1; i < tokens.Length - 1; i++)
{
lstUsers.Items.Add(tokens[i].Trim());
}
}
else if (tokens[0] == "JOIN")
{
//此时从服务器返回的消息格式:命令标志符JOIN| 刚刚登入的用户名
add(tokens[1] + "已经进入了聊天室");
this.lstUsers.Items.Add(tokens[1]);
if (this.username.Text == tokens[1])
{
this.state = CONNECTED;
}
}
else if (tokens[0] == "QUIT")
{
if (this.lstUsers.Items.IndexOf(tokens[1]) > -1)
{
this.lstUsers.Items.Remove(tokens[1]);
}
add("用户:" + tokens[1] + "已经离开");
}
else
{
//如果从服务器返回的其他消息格式,则在ListBox控件中直接显示
add(msg);
}
}
//关闭连接
tcpClient.Close();
}
catch
{
add("网络发生错误");
}
}
//将“EXIT”命令发送给服务器,此命令格式要与服务器端的命令格式一致
private void FrmClient_FormClosing(object sender, FormClosingEventArgs e)
{
btnExit_Click(sender, e);
}
//设置字体颜色
//向显示消息的rtbMsg中添加信息是通过add函数完成的
private void add(string msg)
{
if (!color.IsEmpty)
{
this.rtbMsg.SelectionColor = color;
}
this.rtbMsg.SelectedText = msg + "\n";
}
private void btnExit_Click(object sender, EventArgs e)
{
if (true)
{
string message = "EXIT|" + this.username.Text + "|";
//将字符串转化为字符数组
byte[] outbytes = System.Text.Encoding.Default.GetBytes(message.ToCharArray());
Stream.Write(outbytes, 0, outbytes.Length);
this.state = CLOSED;
this.stopFlag = true;
this.lstUsers.Items.Clear();
}
}
private void btnColor_Click(object sender, EventArgs e)
{
ColorDialog colorDialog1 = new ColorDialog();
colorDialog1.Color = this.rtbMsg.SelectionColor;
if (colorDialog1.ShowDialog() == System.Windows.Forms.DialogResult.OK &&
colorDialog1.Color != this.rtbMsg.SelectionColor)
{
this.rtbMsg.SelectionColor = colorDialog1.Color;
color = colorDialog1.Color;
}
}
}
}