网络聊天室实现报告

网络聊天室实现报告
网络聊天室实现报告

一、背景知识

在传统生活里,人们利用写信、电话等方式联络,但此类方式周期缓慢,呆板且不可靠。在这个信息极其发达的时代,随后我们可以用来进行信息交流的方法日益增多,比如电报、电话、电子邮件、OICQ等通讯手段,但是这些或者不方便或者有局限性或者有费用的问题。

近年来计算机技术的快速发展,特别是计算机网络的发展,越来越深刻的改变了人们生活的方方面面,使得人们能以更低廉的价格,开发出更方便、更实用的网络工具。各种在线服务系统,更是深刻的影响了人们的联系和交流方式,使得人们可以在远隔千里之遥随时通讯。过去的种种陈旧的联系方式,已经不能满足现代生活的需要。

网络聊天室凭借其友好的外观、强大的功能、使用的便利、联系的及时等特点博得现代人的青睐,其应用的市场十分广阔。本系统使用的是C/S模式,使用C#进行聊天室的设计与开发。

本文主要介绍了所应用到的技术的基础知识,并探讨了建立聊天室的设计思想、方法与功能实现流程图。本文所实现的聊天室具有良好的人机交互界面、合理的数据库结构可以实现发言、自动显示所在聊天室的成员等交互功能,经过测试调试,证明可实际应用。下图为网络聊天室的主界面

图1网络聊天室主运行界面图

二、核心算法思想

实现一个基于Socket的简易的聊天室,实现的思路如下:

聊天室服务器端启动服务器时,将创建侦听套接字,创建用户列表,创建并启动侦听线程。用户登录时,将创建套接字,与服务器直接连接,并创建客户端接收线程。服务器端侦听到有用户上线后,将创建新的用户节点,并在主界面上显示用户上线,发送新的用户列表。客户端发送信息时,将要发送的内容进行发送。服务器端发送信息时,如果是发送给所有人,就遍历用户链表,如果是发送给某个用户,先在链表中找到该节点,再发送信息。服务器端和客户端接收信息时,先读取聊天信息标识,做出判断后,依次读取信息,处理信息,并在主界面上显示,服务器端还要将准备好的信息发送给指定的用户。

开启客户端主界面后,就会启动文件接收侦听线程,如果有用户发送文件至此,将会有信息提示,确定接收后,将启动文件接收线程,对方用户端将启动文件发送线程。

服务器端侦听到有用户下线后,将删除该用户节点,并在主界面上显示用户下线,发送新的用户列表。服务器端停止服务后,也会向客户端发送服务器已关闭的信息,客户端将不再可以聊天。

程序的结构:多个客户端+一个服务端,客户端都是向服务端发送消息,然后服务端转发给所有的客户端,这样形成一个简单的聊天室功能。

实现的细节:服务端启动一个监听套接字。每一个客户端连接到服务端,都是开启了一个线程,线程函数是封装了通信套接字,来实现与客户端的通信。多个客户端连接时产生的通信套接字用一个静态的Dictionary保存。下面讲述几个重要概念:

套接字基本概念:

套接字是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。可以将套接字看作不同主机间的进程进行双向通信的端点,它构成了单个主机内及整个网络间的编程界面。套接字存在于通信域中,通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套接字通常和同一个域中的套接字交换数据(数据交换也可能穿越域的界限,但这时一定要执行某种解释程序)。各种进程使用这个相同的域互相之间用Internet协议簇来进行通信。

套接字工作原理:

要通过互联网进行通信,你至少需要一对套接字,其中一个运行于客户机端,我们称之为ClientSocket,另一个运行于服务器端,我们称之为ServerSocket。

根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。

所谓服务器监听,是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。

所谓客户端请求,是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。

所谓连接确认,是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

三、核心算法流程图

(1)在服务器上运行服务器端的应用程序,该程序一运行就开始服务器监听。然后,在客户机上就可以打开客户端的应用程序。程序打开后可以与服务器端应用程序进行连接,即进行客户端请求。

图2服务端与客户端互连流程图

(2)服务器运行原理:

当有客户端连接聊天室服务器后,服务器立刻为这个客户建立一个数据接收的线程(多用户程序必备)。在接收线程中,如果收到聊天命令,就对其进行解析处理,服务器可以处理五种命令:CONN\LIST\CHAT\PRIV\EXIT。

图3服务器运行流程图

(2)聊天室客户端的原理:

当客户端连接到服务器后,服务器立刻建立一个数据接收的独立线程。在接收线程中,如果收到了聊天命令,就对其进行解析处理。聊天室客户端一共处理的命令有五种:OK\ERR\LIST\JOIN\QUIT命令。

图4 客户端流程图

四、源代码

using System;

using System.Collections.Generic;

using System.ComponentModel;

usingSystem.Data;

using System.Drawing;

using System.Linq;

usingSystem.Text;

using System.Windows.Forms;

using System.Net;

using System.Threading;

usingSystem.Net.Sockets;

usingSystem.Diagnostics;

using System.IO;

namespaceChatRoom

{

publicpartial classChatRoom: Form

{

Socket_mysocket;

stringmyName;

bool check;

Thread threadRecevie;

Dictionary DictionaryChatToOne=newDictionary<string,ChatToOne>();

publicChatRoom(Socket mysocket,string_myName)

{

InitializeComponent();

Control.CheckForIllegalCrossThreadCalls= false;

_mysocket= mysocket;

myName= _myName;

check =true;

threadRecevie = newThread(newThreadStart(receive));

threadRecevie.Start();

string sendChar= "}join{"+ _myName.Trim();

byte[] sendByte=Encoding.Unicode.GetBytes(sendChar.ToArray());

NetworkStream netstr = new NetworkStream(mysocket);

netstr.Write(sendByte, 0,sendByte.Length);

netstr.Close();

protected delegate void_delegate(stringstr_redata); //定义一个委托

protected void_delegate1(string str_redata)

{

string chatStr = str_redata.Replace("{wrterChatToOne}", "");

string[] str = chatStr.Split(newstring[]{ "{SayWord}"}, StringSplitOptions.None);

string strMesg = str[1];

string receiveName = str[0];

if(DictionaryChatToOne.Keys.Contains(receiveName))

{

if (DictionaryChatToOne[receiveName].IsDisposed)

ChatToOnefm = new ChatToOne(receiveName);

fm.myscoket = _mysocket;

fm.myName= myName;

DictionaryChatToOne[receiveName] = fm;

fm.Show();

}

else

{

DictionaryChatToOne[receiveName].Activate();

DictionaryChatToOne[receiveName].WindowState=Fo

rmWindowState.Normal;

}

}

else

{

ChatToOne fm =newChatToOne(receiveName);

fm.myscoket = _mysocket;

fm.myName =myName;

DictionaryChatToOne.Add(receiveName, fm);

fm.Show();

}

string nameDate = str[0]+" " +DateTime.Now.T oString("yyyy/MM/dd HH:mm:ss");

((ChatToOne)DictionaryChatToOne[str[0]]).FchatStr(nameDate,strMesg);

return;

internalstatic Hashtableclients= new Hashtable();//clients数组保存当前在线用户的client对象

private TcpListener listener;//该服务器默认的监听端口号

staticint MAX_NUM=100;//服务器可以支持的客户端的最大连接数

internal static bool SocketServiceFlag = false;//开始服务的标志

//获得本地局域网或者拨号动态分配的IP地址,在启动服务器时会用到IP地址

private string getIPAddress()

{

//获得本机局域网IP地址

IPAddress[]Addresslist=Dns.GetHostEntry(Dns.GetHostNam

e()).AddressList;

if (Addresslist.Length<1)

{

return"";

}

return Addresslist[0].ToString();

}

//获得动态的IP地址

private staticstring getDynamicIPAddress()

{

IPAddress[] Addresslist =Dns.GetHostEntry(Dns.GetHostName()).AddressList;

if(Addresslist.Length < 2)

{

return"";

}

return Addresslist[1].ToString();

//服务器监听的端口号通过getValidPort()函数获得

private int getValidPort(string port)

intlport;

//测试端口号是否有效

try

{

//是否为空

if (port == "")

throw newArgumentException("端口号为空,不能启动服务器");

}

lport = System.Convert.ToInt32(port);

}

catch (Exception e)

{ Console.WriteLine("无效的端口号:"+ e.ToString());

this.rtbSocketMsg.AppendText("无效的端口号:"+ e.ToString() + "\n");

return -1;

}

return lport;

privatevoid btnSocketStart_Click(objectsender,EventArgs e)

{

intport = getValidPort(tbSocketPort.Text);

if (port < 0)

{

return;

string ip=this.getIPAddress();

try

{

IPAddress ipAdd =IPAddress.Parse(ip);

listener.Start(); //开始监听服务器端口

this.rtbSocketMsg.AppendText("Socket服务器已经启动,正在监听"

+ ip + "端口号:"+this.tbSocketPort.Text + "\n");

//启动一个新的线程,执行方法this.StartSocketListen,

//以便在一个独立的进程中执行确认与客户端Socket连接的操作

Form1.SocketServiceFlag = true;

Thread thread = new Thread(new ThreadStart(this.StartS ocketListen));

thread.Start();

this.btnSocketStart.Enabled = false;

this.btnSocketStop.Enabled = true;

}

catch(Exception ex)

{

this.rtbSocketMsg.AppendText(ex.Message.ToString()+ "\n");

}

}

//在新的线程中的操作,它主要用于当接收到一个客户端请求时,确认与客户端的链接

//并且立刻启动一个新的线程来处理和该客户端的信息交互

private voidStartSocketListen()

while(Form1.SocketServiceFlag)

{

try

{ //当接收到一个客户端请求时,确认与客户端的链接

if(listener.Pending())//确认是否有挂起的连接请求

{

Socketsocket =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(t his,socket);

ThreadclientService = newThread(new ThreadStart(client.ServiceClient));

clientService.Start();

}

Thread.Sleep(200);//提高性能整体速度

catch (Exception ex)

this.rtbSocketMsg.AppendText(ex.Message.ToStri ng() + "\n");

}

//client定义

public class Client

private string name;//保存用户名

private Socket currentSocket = null;//保存与当前用户连接的Socket对象

private string ipAddress;//保存用户的IP地址

private Form1 server;

//保存当前连接状态

//Closed--connected--closed

privatestring 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.GetByte

s(msg.ToCharArray());

client.currentSocket.Send(message, message.Length, 0);

//ServiceClient 方法用于和客户端进行数据通信,包括接收客户端的请求

//它根据不同的请求命令执行相应的操作,并将处理结果返回到客户端

//ServiceClient()函数为服务器接收客户数据的线程主体,主要用来接收用户发送来的数据,并处理聊天命令

public void ServiceClient()

{

string[] tokens=null;

byte[] buff=newbyte[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数组中

intlen = currentSocket.Receive(buff);

//将字符数组转化为字符串

string clientCommand=System.Text.Encoding.Default.GetString(b uff,0,len);

//tokens【0】中保存了命令标志符(CONNCHAT PRIV LIST 或 EXIT)

tokens=clientCommand.Split(new char[]{'|'});

if(tokens==null)

{

Thread.Sleep(200);

continue;

}

}

catch(Exceptione)

{

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"+thihttps://www.360docs.net/doc/c64453149.html,+"

已经存在");

else

{

HashtablesyncClients=Hashtable.Synchronized(Form1.clients);

syncClients.Add(this.name,this);

//更新界面

server.addUser(thhttps://www.360docs.net/doc/c64453149.html,); //对每一个当前在线的用户发//送JOIN消息命令和LIST消息命令,以此来跟新客户端的当前在线用户列表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类(用于与服务器的连接)

TcpClienttcpClient;//与服务器的链接

private NetworkStream Stream;//与服务器数据交互的流通道

private static stringCLOSED = "closed";

privatestatic stringCONNECTED = "connected";

private stringstate = CLOSED;

private bool stopFlag;

privateColorcolor;//保存当前客户端显示的颜色

//连接聊天室服务器

//通过TcpClient方法连接聊天室服务器并发送CONN消息命令

private void btnLogin_Click_1(object sender, EventArgs e) {

if (state == CONNECTED)

{

return;

}

if (this.tbUserName.TextLength== 0)

{

MessageBox.Show("请输入您的昵称!","提示信息", MessageBoxBu ttons.OK, MessageBoxIcon.Exclamation);

this.tbUserName.Focus();//为控件设置焦点,人性化设计

return;

}

try

{

//创建一个客户端套接字,它是Login的一个公共属性

tcpClient = new TcpClient();//将被传递给ChatClient窗体 }

private voidbtnSend_Click_1(object sender,EventArgs e)

{

try

{

if (!this.cbPrivate.Checked)

{

//此时命令的格式是:命令标识符CHAT|发送者的用户名:发送内容|

stringmessage = "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|发送者的用户名|接收者的用户名|发送内容

stringmessage ="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(newchar[]{ '|' });

//tokens[0]中保存了命令标志符LIST JOIN QUIT

if (tokens[0].ToUpper() == "OK")

{

//处理响应

add("命令执行成功!");

else if (tokens[0].ToUpper() == "ERR")

add("命令执行错误:"+ tokens[1]);

}

elseif (tokens[0] =="LIST")

{

//此时从服务器返回的消息格式:命令标志符LIST|用户名1|用户名2|。。(有在线用户名)

//add(“获得用户列表”),更新在线用户列表

lstUsers.Items.Clear();

for(inti = 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.GetByt es(message.ToCharArray());

Stream.Write(outbytes, 0, outbytes.Length);

this.state = CLOSED;

this.stopFlag = true;

this.lstUsers.Items.Clear();

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

private void Form1_FormClosing(objectsender, FormClosingEventArgse)

{

// btnExit_Click_1(sender,e);

btnExit_Click_1(sender, e);

}

private void btnColor_Click(object sender,EventArgs e)

ColorDialog colorDialog1 = newColorDialog();

colorDialog1.Color =this.rtbMsg.SelectionColor;

if (colorDialog1.ShowDialog() == System.Window

s.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.Defaul t.GetBytes(message.ToCharArray());

//将字符串转化为字符数组

Stream.Write(outbytes, 0, outbytes.Length);

}

}

catch

{

this.rtbMsg.AppendText("网络发生错误!");

}

}

//this.ServerResponse()方法用于接收从服务器发回的信息,根据不同的命令,执行相应的操作

privatevoidServerResponse()

{

//定义一个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(newchar[] { '|'});//分离字符

//tokens[0]中保存了命令标志符LIST JOIN QUIT

if (tokens[0].ToUpper()== "OK")

//处理响应

add("命令执行成功!");

}

elseif (tokens[0].ToUpper() == "ERR")

{

add("命令执行错误:"+ tokens[1]);

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

}

elseif (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("网络发生错误");

}

五、心得体会

C#面向对象设计是一门应用广泛并且实用性强的高级程序设计语言,通过本次实验,我对项目的各个阶段的任务有了一定的了解,设计开始阶段必须明确设计的目的与需求分析,总体设计要全面分析聊天系统的架构。

经过本次专题训练,我感受颇多,开始面对这个的题目时,有些不知所措,都不知道如何下手,后来去图书馆借了几本相关的书籍,心里才感觉有了一些底。经过这些天的学习与编码,总算收获不少,对C#的认识更加的深刻了,而且,对TCP/IP协议与网络编程技术的实

相关主题
相关文档
最新文档