网络聊天室实现报告
一、背景知识
在传统生活里,人们利用写信、电话等方式联络,但此类方式周期缓慢,呆板且不可靠。在这个信息极其发达的时代,随后我们可以用来进行信息交流的方法日益增多,比如电报、电话、电子邮件、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 https://www.360docs.net/doc/d27802073.html,ponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using https://www.360docs.net/doc/d27802073.html,;
using System.Threading;
using https://www.360docs.net/doc/d27802073.html,.Sockets;
using System.Diagnostics;
using System.IO;
namespace ChatRoom
{
public partial class ChatRoom : Form
{
Socket _mysocket;
string myName;
bool check;
Thread threadRecevie;
Dictionary
Dictionary
public ChatRoom(Socket mysocket,string _myName)
{
InitializeComponent();
Control.CheckForIllegalCrossThreadCalls = false;
_mysocket = mysocket;
myName = _myName;
check = true;
threadRecevie = new Thread(new ThreadStart(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(string str_redata); //定义一个委托 protected void _delegate1(string str_redata)
{
string chatStr = str_redata.Replace("{wrterChatToOne}", "");
string[] str = chatStr.Split(new string[] { "{SayWord}" }, StringSplitOptions.None);
string strMesg = str[1];
string receiveName = str[0];
if (DictionaryChatToOne.Keys.Contains(receiveName)) { if (DictionaryChatToOne[receiveName].IsDisposed)
{
ChatToOne fm = new ChatToOne(receiveName);
fm.myscoket = _mysocket;
fm.myName = myName;
DictionaryChatToOne[receiveName] = fm;
fm.Show();
}
else
{
DictionaryChatToOne[receiveName].Activate();
DictionaryChatToOne[receiveName].WindowState =
FormWindowState.Normal;
}
}
else
{
ChatToOne fm = new ChatToOne(receiveName);
fm.myscoket = _mysocket;
fm.myName = myName;
DictionaryChatToOne.Add(receiveName, fm); fm.Show();
}
string nameDate = str[0] + " " + DateTime.Now.ToString("yyyy/MM/dd
HH:mm:ss");
((ChatToOne)DictionaryChatToOne[str[0]]).FchatStr(nameDate, strMesg);
return;
}
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.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(newThreadStart(client.ServiceClient));
clientService.Start();
}
}
Thread.Sleep(200);//提高性能整体速度
catch (Exception ex)
{
this.rtbSocketMsg.AppendText(ex.Message.ToString() + "\n");
}
}
}
//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();
}