当前位置:文档之家› 基于Socket的聊天室(C#版)

基于Socket的聊天室(C#版)

基于Socket的聊天室(C#版)
基于Socket的聊天室(C#版)

一、服务器/客户端聊天室模型

聊天室客户端(商用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————网络应用程序

电话机————访问通信协议

聊天协议的应答:

聊天状态: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命令。

五、程序设计(代码)

服务器端设计:

引入网络操作命名空间https://www.doczj.com/doc/2c4484749.html,、https://www.doczj.com/doc/2c4484749.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]中保存了发送者的用户名

https://www.doczj.com/doc/2c4484749.html,=tokens[1];

if(Form1.clients.Contains(https://www.doczj.com/doc/2c4484749.html,))

{

SendToClient(this,"ERR|User"+https://www.doczj.com/doc/2c4484749.html,+"已经存在");

}

else

{

Hashtable syncClients=Hashtable.Synchronized(Form1.clients);

syncClients.Add(https://www.doczj.com/doc/2c4484749.html,,this);

//更新界面

server.addUser(https://www.doczj.com/doc/2c4484749.html,);

//对每一个当前在线的用户发送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.doczj.com/doc/2c4484749.html,);

server.removeUser(https://www.doczj.com/doc/2c4484749.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.IndexOf(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)

{

string message = "EXIT|" + this.tbUserName.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();

}

}

//将“EXIT”命令发送给服务器,此命令格式要与服务器端的命令格式一致

private void Form1_FormClosing(object sender, FormClosingEventArgs e)

{

// btnExit_Click_1(sender,e);

btnExit_Click_1(sender, e);

}

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;

}

}

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);

相关主题
文本预览
相关文档 最新文档