资源描述
一、服务器/客户端聊天室模型
1.首先启动聊天室服务器,使得TcpListener开始监听端口,此时TcpListener会进入Pending状态,等待客户端连接;
2.其次,当有客户端连接后,通过AccepSocket返回与客户端连接的Socket对象,然后通过读写Socket对象完成与聊天室客户端的数据传输。聊天室客户端成功启动后,首先创建一个Socket对象,然后通过这个Socket对象连接聊天室服务器,连接成功后开通Socket完成数据的接收和发送处理。
二、系统功能设计
本设计为一个简单的聊天室工具,设计基本的聊天功能,如聊天、列表维护等。系统主要为两大块:聊天室服务器及聊天室客户端。
服务器界面设计如下:
客户端界面设计如下:
三、聊天协议的应答
A—网络—B
主机与主机通信主要识别身份(标识设备用IP)及通信协议
网络应用程序——端口号——接收数据
注:1.IP地址是总机,端口号是分机(传输层)
2.端口号为16位二进制数,范围0到65535,但实际编程只能用1024以上端口号
Socket编程
首先,我们了解常用网络编程协议。我们用得最多的协议是UDP和TCP,UDP是不可靠传输服务,TCP是可靠传输服务。UDP就像点对点的数据传输一样,发送者把数据打包,包上有收信者的地址和其他必要信息,至于收信者能不能收到,UDP协议并不保证。而TCP协议就像(实际他们是一个层次的网络协议)是建立在UDP的基础上,加入了校验和重传等复杂的机制来保证数据可靠的传达到收信者。一个是面向连接一个无连接,各有用处,在一些数据传输率高的场合如视频会议倾向于UDP,而对一些数据安全要求高的地方如下载文件就倾向于TCP。
Socket————网络应用程序
电话机————访问通信协议
聊天协议的应答:
聊天状态: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命令。
五、程序设计(代码)
服务器端设计:
引入网络操作命名空间System.Net、System.Net.Sockets;
线程处理命名空间System.Threading
第一步:界面设计及类与相关成员的定义
对界面进行设计(简单)
对内部函数进行设计(要编写一个独立的类即Client类,封装了客户端的信息与连接,每一个客户进入聊天室,就创建一个Client对象,用于保存该用户的信息并接收用户数据和发送信息到客户端)
几个重要的类:TcpListener类(服务器套接字创建)、Socket类
internal static Hashtable clients = new Hashtable();//clients数组保存当前在线用户的client对象
private TcpListener listener;//该服务器默认的监听端口号
static int MAX_NUM = 100; //服务器可以支持的客户端的最大连接数
internal static bool SocketServiceFlag = false;//开始服务的标志
//获得本地局域网或者拨号动态分配的IP地址,在启动服务器时会用到IP地址
private string getIPAddress()
{
//获得本机局域网IP地址
IPAddress[] Addresslist=Dns.GetHostEntry(Dns.GetHostName()).AddressList;
if (Addresslist.Length<1)
{
return "";
}
return Addresslist[0].ToString();
}
//获得动态的IP地址
private static string getDynamicIPAddress()
{
IPAddress[] Addresslist = Dns.GetHostEntry(Dns.GetHostName()).AddressList;
if (Addresslist.Length < 2)
{
return "";
}
return Addresslist[1].ToString();
}
//服务器监听的端口号通过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.rtbSocketMsg.AppendText("无效的端口号:" + e.ToString() + "\n");
return -1;
}
return lport;
}
private void btnSocketStart_Click(object sender, EventArgs e)
{
int port = getValidPort(tbSocketPort.Text);
if (port < 0)
{
return;
}
string ip = this.getIPAddress();
try
{
IPAddress ipAdd = IPAddress.Parse(ip);
listener = new TcpListener(ipAdd, port);//创建服务器套接字
listener.Start(); //开始监听服务器端口
this.rtbSocketMsg.AppendText("Socket服务器已经启动,正在监听"
+ ip + "端口号:" + this.tbSocketPort.Text + "\n");
//启动一个新的线程,执行方法this.StartSocketListen,
//以便在一个独立的进程中执行确认与客户端Socket连接的操作
Form1.SocketServiceFlag = true;
Thread thread = new Thread(new ThreadStart(this.StartSocketListen));
thread.Start();
this.btnSocketStart.Enabled = false;
this.btnSocketStop.Enabled = true;
}
catch (Exception ex)
{
this.rtbSocketMsg.AppendText(ex.Message.ToString() + "\n");
}
}
//在新的线程中的操作,它主要用于当接收到一个客户端请求时,确认与客户端的链接
//并且立刻启动一个新的线程来处理和该客户端的信息交互
private void StartSocketListen()
{
while (Form1.SocketServiceFlag)
{
try
{
//当接收到一个客户端请求时,确认与客户端的链接
if (listener.Pending())//确认是否有挂起的连接请求
{
Socket socket = listener.AcceptSocket();//接收挂起的连接请求
if (clients.Count >= MAX_NUM)
{
this.rtbSocketMsg.AppendText("已经达到了最大连接数:" + MAX_NUM + ",拒绝新的链接\n");
socket.Close();
}
else
{
//启动一个新的线程
//执行方法this.ServiceClient,处理用户相应的请求
ChatSever.Client.Client client = new ChatSever.Client.Client(this, socket);
Thread clientService = new Thread(new ThreadStart(client.ServiceClient));
clientService.Start();
}
}
Thread.Sleep(200);//提高性能整体速度,原因不详
}
catch (Exception ex)
{
this.rtbSocketMsg.AppendText(ex.Message.ToString() + "\n");
}
}
}
private void tbSocketPort_TextChanged(object sender, EventArgs e)
{
if (this.tbSocketPort.Text!="")
{
this.btnSocketStart.Enabled = true;
}
}
//下面为一些界面处理函数
private void btnSocketStop_Click(object sender, EventArgs e)
{
Form1.SocketServiceFlag = false;
this.btnSocketStart.Enabled = true;
this.btnSocketStop.Enabled = false;
}
public void addUser(string username)
{
this.rtbSocketMsg.AppendText(username + "已经加入\n");//将刚连接的用户名加入到当前在线用户列表中
this.lbSocketClients.Items.Add(username);
this.tbSocketClientsNum.Text = System.Convert.ToString(clients.Count);
}
public void removeUser(string username)
{
this.rtbSocketMsg.AppendText(username + "已经离开\n");//将刚连接的用户名加入到当前在线用户列表中
this.lbSocketClients.Items.Remove(username);
this.tbSocketClientsNum.Text = System.Convert.ToString(clients.Count);
}
public string GetUserList()
{
string Rtn = "";
for (int i = 0; i < lbSocketClients.Items.Count; i++)
{
Rtn += lbSocketClients.Items[i].ToString() + "|";
}
return Rtn;
}
public void updateUI(string msg)
{
this.rtbSocketMsg.AppendText(msg + "\n");
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
Form1.SocketServiceFlag = false;
}
//下面为Client类定义
public class Client
{
private string name;//保存用户名
private Socket currentSocket = null;//保存与当前用户连接的Socket对象
private string ipAddress;//保存用户的IP地址
private Form1 server;
//保存当前连接状态
//Closed--connected--closed
private string state = "closed";
public Client(Form1 server, Socket clientSocket)
{
this.server = server;
this.currentSocket = clientSocket;
ipAddress = getRemoteIPAddress();
}
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
public Socket CurrentSocket
{
get
{
return currentSocket;//ipAddress
}
}
private string getRemoteIPAddress()
{
return ((IPEndPoint)currentSocket.RemoteEndPoint).Address.ToString();
}
//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);
}
//ServiceClient 方法用于和客户端进行数据通信,包括接收客户端的请求
//它根据不同的请求命令执行相应的操作,并将处理结果返回到客户端
//ServiceClient()函数为服务器接收客户数据的线程主体,主要用来接收用户发送来的数据,并处理聊天命令
public void ServiceClient()
{
string[] tokens=null;
byte[] buff=new byte[1024];
bool keepConnect=true;
//用循环来不断地与客户端进行交互,直到客户端发出“EXIT”命令
//将keepConnect职为false,退出循环,关闭连接,并中止当前线程
while(keepConnect&&Form1.SocketServiceFlag)
{
//tokens=null;
try
{
if(currentSocket==null||currentSocket.Available<1)
{
Thread.Sleep(300);
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(200);
continue;
}
}
catch(Exception e)
{
server.updateUI("发送异常:"+e.ToString());
}
}
//以上代码主要用于服务器初始化和接收客户端发送来的数据。它在对用户数据进行解析后,把用户命令转换为数组方式。
if(tokens[0]=="CONN")
{
//此时接收到的命令格式化为命令标识符CONN|发送者的用户名|tokens[1]中保存了发送者的用户名
this.name=tokens[1];
if(Form1.clients.Contains(this.name))
{
SendToClient(this,"ERR|User"+this.name+"已经存在");
}
else
{
Hashtable syncClients=Hashtable.Synchronized(Form1.clients);
syncClients.Add(this.name,this);
//更新界面
server.addUser(this.name);
//对每一个当前在线的用户发送JOIN消息命令和LIST消息命令,以此来跟新客户端的当前在线用户列表
System.Collections.IEnumerator myEnumerator=Form1.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]=="CHAT")
{
if (state =="connected")
{
//此时收到的命令的格式为:命令标识符CHAT|发送者的用户名:发送内容|向所有当前在线的用户转发此信息
System.Collections.IEnumerator myEnumerator=Form1.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 (Form1.clients.Contains(sender))
{
SendToClient((Client)Form1.clients[sender],message);
}
if (Form1.clients.Contains(receiver))
{
SendToClient((Client)Form1.clients[receiver],message);
}
server.updateUI(tokens[1]);
}
else
{
//send err to server
SendToClient(this,"ERR|state error,please login first");
}
}
else if (tokens[0]=="EXIT")
{ //此时收到的命令的格式为:命令标识符EXIT|发送者的用户名:发送内容|
//向所有当前在线的用户发送该用户已离开的消息
if(Form1.clients.Contains(tokens[1]))
{
Client client=(Client)Form1.clients(tokens[1]));
//将该用户对应Client对象从clients中删除
Hashtable syncClients=Hashtable.Synchronized(Form1.clients);
syncClients.Remove(client.name);
server.removeUser(client.name);
//向客户端发送QUIT命令
string message ="QUIT|"+tokens[1];
展开阅读全文