基于Socket的聊天室C#版.docx
一、服务器 / 客户端聊天室模型
聊天室客户端
(其他)
聊天室客户端(笔记
本)聊天室客户端(商用PC)
服务器
其他服务器
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 ————网络应用程序
电话机————访问通信协议
聊天协议的应答:
聊天命令
客户端服务器
OK /ERR 应答信号
聊天状态: CLOSED 和 CONNECTED 状态
执行 CONN 命令后进入 CONNECTED 状态,执行下列命令:
CONN: 连接聊天室服务器
JOIN: 加入聊天(通知其他用户本人已经加入聊天室服务器)
LIST:列出所有的用户(向客户端发送全部的登录用户名字)
CHAT:发送聊天信息(公开的聊天信息)
PRIV:进行私聊(三个参数:私聊信息用户;接收私聊信息用户;发送信息)EXIT:客户端向服务器发送离开请求;
QUIT: 退出聊天,服务器向客户端发送退出命令(执行QUIT 命令聊天状态变为CLOSED)
四、系统实现
服务器协议解析:
当有客户端连接聊天室服务器后,服务器立刻为这个客户建立一个数据接收
的线程(多用户程序必备)。在接收线程中,如果收到聊天命令,就对其进行解
析处理,服务器可以处理五种命令:CONN\LIST\CHAT\PRIV\EXIT。
服务器接收到 CONN 命令,就向其他用户发送JOIN 命令告诉有用户加入,然后把当前的全部用户信息返回给刚刚加入的用户,以便在界面上显示用户列
表。当接收到 EXIT 命令后,就清除当前用户的信息,然后向其他用户发送QUIT 命令,告诉其他用户退出了,这些用户的客户端把离开的用户从用户列表中删除。
启动聊天服务器
启动监听器
等待
接收客户端连接
启动客户数据接收线程
保持连接并且SocketServiceFlag为 true ?
退出线程
读取数据
解析命令
CONN命令LIST命令CHAT命令PRIV 命令EXIT命令
向全部用户发向接收者发送
删除用户数据
送 JOIN命令向当用户发送向当用户发送数据
LIST命令CHAT命令
向当用户发送向发送者发送向全部用户发
LIST命令数据送 QUIT命令
休息 200毫秒
聊天室客户端的协议解析:
当客户端连接到服务器后,服务器立刻建立一个数据接收的独立线程。在接收线程中,如果收到了聊天命令,就对其进行解析处理。聊天室客户端一共处理的命令有五种: OK\ERR\LIST\JOIN\QUIT命令。
启动聊天室客
户端
Socket
Connect
启动客户数据接收线程
Yes
是停止标志吗?
退出线程
No
读取数据
解析命令
QUIT 命令
其他情况OK 命令ERR命令LIST 命令JOIN命令
删除用户数据
命令执行成功命令执行失败显示全部用户显示用户加入直接显示
用户信息
显示用户离开
休息 200毫秒
五、程序设计(代码)
服务器端设计:
引入网络操作命名空间https://www.360docs.net/doc/aa13332914.html, 、https://www.360docs.net/doc/aa13332914.html,.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(https://www.360docs.net/doc/aa13332914.html,);
server.removeUser(https://www.360docs.net/doc/aa13332914.html,);
// 向客户端发送 QUIT命令
string message = "QUIT|" +tokens[1];
System.Collections.IEnumerator
myEnumerator=Form1.clients.Values.GetEnumerator();
while (myEnumerator.MoveNext())
{
Client c =( Client)myEnumerator.Current;
// 将发送者的用户名:发送内容转发给用户
SendToClient(c,message);
}
server.updateUI("QUIT" );
}
// 退出当前线程break;
}
Thread .Sleep(200);
}
客户端设计:
包含一个类 ChatClientForm,该类封装了聊天室客户端界面和聊天命令处理逻辑。
其中一个重要的类TcpClient类(用于与服务器的连接)
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;// 保存当前客户端显示的颜色
//连接聊天室服务器
//通过 TcpClient 方法连接聊天室服务器并发送 CONN消息命令
private void btnLogin_Click_1(object sender, EventArgs e)
{
if(state == CONNECTED)
{
return ;
}
if ( this .tbUserName.TextLength == 0)
{
MessageBox.Show( " 请输入您的昵称!" , " 提示信息 " , MessageBoxButtons .OK, MessageBoxIcon.Exclamation);
this .tbUserName.Focus(); // 为控件设置焦点,人性化设计
return ;
}
try
{
// 创建一个客户端套接字,它是Login 的一个公共属性
tcpClient =new TcpClient (); // 将被传递给 ChatClient窗体
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 .tbUserName.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_1( object sender, EventArgs e)
{
try
{
if (! this .cbPrivate.Checked)
{
//此时命令的格式是:命令标识符 CHAT|发送者的用户名:发送内容 |
string message = "CHAT|" + this .tbUserName.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 .tbUserName.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(200);
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 .tbUserName.Text == tokens[1])
{
this .state = CONNECTED;
}
}
else if (tokens[0] =="QUIT" )
{
if
( this .lstUsers.Items.I
ndexOf(tokens[1]) > -1)
{
this .lstUsers.Items.Remove(tokens[1]);
}
add(" 用户: " + tokens[1] +" 已经离开 " );
}
else
{
// 如果从服务器返回的其他消息格式,则在ListBox 控件中直接显示// this.rtbMsg.SelectedText = msg + "\n";
add(msg);
}
}
//关闭连接
tcpClient.Close();
}
catch
{
add(" 网络发生错误");
}
}
//设置字体颜色
//向显示消息的 rtbMsg 中添加信息是通过 add函数完成的
private void add( string msg)
{
if(!color.IsEmpty)
{
this .rtbMsg.SelectionColor = color;
}
this .rtbMsg.SelectedText = msg +"\n" ;
}
private void btnExit_Click_1(object sender, EventArgs e)
{
if ( true )