案例是一个网络聊天工具的实例。使用该系统可以完成的功能,包括用户登陆、显示所有当前在线的用户、显示进行聊天的用户的信息、与选定的用户进行聊天,并且支持私聊。读者可以结合此实例,对其功能和界面进行完善和优化,从而把它应用到自己的应用程序中去。本章旨在通过介绍这个实例程序的设计和实现,讲解使用Visual C#.NET中Socket编程的关键技术以及多线程的指示进行高级应用程序开发的方法。
6.1提出问题
在本章的实例中,我们实现了一个简单的再现聊天工具,它提供了基本的聊天功能。它能够维持在线用户列表,支持私聊等功能。通过本章的学习,读者可以学习到如何利用Socket建立服务器程序和客户端程序以及关于多进程间调度方面的知识。下面让我们先看一下网络聊天工具实例的演示。
6.1.1 实例演示
在本章的实例中服务器端是一个Windows窗体应用程序——ChatServer,在客户端也是一个Windows窗体应用程序——ChatClient。当运行ChatServer.exe时,便运行服务器应用程序,单击“启动”按钮后便启动了服务器进程,如图6_1所示。
图6_1 服务器的初始窗口
服务器程序ChatServer负责监听客户端的请求,记录当前的在线用户列表,而客户程序ChatClient负责向服务器发送消息,并接受由服务器中转的由其他用户发送给自己的消息。
我们可以运行多个客户端,客户端之间通过服务器中转的连接方式进行通话。在这里我们运行3次ChatClient.exe分别启动3个客户端进程,首先它们都进入用户登陆界面,如图6-2所示。
图6-2 用户登陆窗体
在登陆界面中第一个窗口输入用户名Mary,第二个窗口输入用户名John,第三个窗口输入用户名Kate,分别单击“登陆”按钮后,分别进入用户Mary,John和Kate的客户端窗口,图6-3显示了John的窗口,其他用户窗口类似,而图6-4显示了用户Mary,John和Kate登陆后的服务器窗口。
图6-3 用户John的初始窗口
图6-4 用户Mary、John和Kate登录后的服务器窗口
用户John这时向所有在线用户发送消息“Hello,everyone!”后,所有的在线用户(Mary,
Kate包括John自己)都会在自己的窗口中显示John所发的消息,图6-5显示Mary收到信息后的窗口。
图6-5 Mary收到John的信息后的窗口
如果Mary想与John私聊,那么可以选中“悄悄话”复选框,同时在当前在线用户列表中选中John, 便进入两人世界。Mary向John发送悄悄话“How ru ? ” 后, 图6-6显示了John 收到信息后的窗口。Mary所发的悄悄话只在她和John的窗口中显示,而Kate的窗口是收不到的。
图6-6 John收到Mary发送的悄悄话后的窗口
好了,有了对网络聊天工具这个简单实例的初步了解,下面先让我们看一下本章所涉及到的关键基础知识。
6.1.2 TCP协议通信的流程
TCP协议是面向连接的协议,它的实现需要数据发送方建立数据通信连接,它的具体的流程如下:
(1) 服务器端首先创建服务器套接字
(2) 服务器套接字监听一个端口,等待客户的请求
(3) 服务器端首先创建服务器套接字
(4) 服务器套接字监听一个端口,等待客户的请求
命令格式 | 说明 |
EXIT|发送者的用户名| | 该命令是用户在客户端程序中单击“离开”按钮后,由客户端程序自动发送服务器应用程序收到该命令后,将发送者的用户名从当前在线用户列表中删除,并给所有当前在线的用户发送更新在线用户列表的命令 |
(5) 服务器的确认与客户端的连接
(6) 客户端和服务器利用建立的连接进行通信
(7) 通信完毕后,客户端和服务器关闭各自的连接
6.1.3 Socket 编程基础
Socket(网络套结字)是网络通信的基本条件,它是可以被命名和寻址的通信端口,使
用中每个Socket都有对应的类型和一个与之相关的进程。下面分别介绍如何利用Socket建立服务器程序和客户端程序。
1. 利用Socket建立服务器程序
要用Socket建方一个TCP服务器程序,一般需要以下几个步骤。
(1)创建一个服务器套结字,用IP地址和端口初始化服务器,代码如下:
IPAddress ipAdd=IPAddress.Parse(“210.77.26.37”);
TcpListener listener=new TcpListener(ipAdd,1234);
(2)监听服务器的端口,代码如下:
Listener.Start();
(3)确认与客户端的连接,代码如下:
Socket socket=listener.AcceptSocket();
(4)处理客户端的请求并回应客户端,下面的代码演示了向客户端发送字符串:
String message=”hello”;
Byte[] outbytes=System.Text.Encoding. ASCII.GetBytes(message.ToCharArray());
Socket.Send(outbytes,outbytes.Length,0);
(5)断开客户端的连接,释放客户端连接,代码如下:
Socket.Close();
(6)关闭服务器,释放服务器的连接,代码如下:
Listener.Close();
2. 利用Socket建立客户端程序
要用Socket建立一个TCP客户端程序,一般需要以下几个步骤。
(1)创建客户端套结字,代码如下:
TcpClient tcpClient=new TcpClient();
(2)连接服务器,代码如下:
tcpClient.Connect(IPAddress.Parse(“210.77.26.37”),1234);
(3)。得到与服务器通信的流通通道,代码如下:
NetworkStream Strm=tcpclient.GetStream();
(4)向服务器发送数据,代码如下:
String cmd=”CONN|”+UserAlias+”|”;
Byte[] outbytes=System.Text.Encoding.ASCII.GetBytes(cmd.ToCharArray());
Strm.Write(outbytes,0,outbytes.Length);
(5)接收从服务器发回的数据,代码如下:
Byte[] buff=new byte[1024];
Int len=Strm.Read(buff,0,buff.Length);
String msg=System.Text.Encoding.ASCII.GetString(buff,0,len);
(6)断开连接,代码如下:
tcpClient.Close();
3.利用Socket服务器和客户端的通信流程
根据上面所述,我们画出了服务器和客户端的通信流程,如图6-7所示:
图6-7 服务器与客户端的通信流程
结合以上的基础知识,下面我们来看一下网络聊天工具这个实例的具体设计思路。
6.2 设计方案
在该节中将讲述缝隙网络聊天工具这个实例是如何设计的,应该用程序的结构为一个客户机/服务器(C/S)结构,它主要包括两个方面的内容设计:
l 服务器端的设计
l 客户端的设计
6.2.1 服务器端的设计
服务器端管理着聊天任务,它维持着一张当前在线用户的列表,转发用户发送来的信息,我们设计的主要功能如下。
l 监听本机IP地址中的一个指定的端口。
l 当有用户端向该端口发送请求时,服务器程序里克建立一个与该客户端的连接并启动一个新的线程来处理该客户端的所有请求。
l 根据客户端发送来的各种不同的请求,执行相应地操作,并将处理结果返回给该客户端。服务器能够识别4种请求命令:CONN(建立新连接)、CHAT(聊天)、PRIV(私聊)EXIT和(离开),服务器接收ASCII字符信息,用“|”分割信息的各个部分,一条信息包含一条命令,一个或多个信息参数,表6-1列出了所有的命令列表。
表6-1 服务器能够识别的命令列表
命令格式 | 说 明 |
CONN用户名 | 该命令在客户端和服务器连接后由客户端程序自动发送 服务器程序收到该命令后便会将“用户名”添加到在线用户列表中,同时向每个在线用户发送更新在线用户列表的命令 |
CHAT发送者的姓名: 发送信息的内容 | 该命令是用户在客户端程序界面中(如图6-3所示)输入发送信息的内容后,单击“发送”按钮后,由客户端程序自动发送 服务器程序收到给命令后便将“发送者的用户名;发送者的内容”转发给所有的当前在线用户 |
PRIV发送者的用户名|接收者的用户名|发送信息的内容
| 该命令是用户在客户端应用程序中选中“悄悄话”复选框(进入私聊的功能,如图6-5所示),输入发送信息内容后单击“发送”按钮后,由客户端程序自动发送 服务器程序收到该命令后,将“发送者的用户名|接收者的用户名|发送信息的内容|”转发给对应的接收者 |
命令格式 | 说明 |
EXIT|发送者的用户名| | 该命令是用户在客户端程序中单击“离开”按钮后,由客户端程序自动发送服务器应用程序收到该命令后,将发送者的用户名从当前在线用户列表中删除,并给所有当前在线的用户发送更新在线用户列表的命令 |
注意:
以上这4条命令可以使读者自己定义的,只有客户端和服务器端的命令请求格式一致,那么读者便可自己定义所有这些命令。
6.2.2 客户端的设计
客户端应用程序包含用户登录窗口和用户聊天的主窗口,它允许用户登录到服务器,可以向服务器发送消息,同时可以接收从服务器返回的信息,我们设计的主要功能如下:
l 向远程服务器发送连接请求。
l 得到服务器程序的确认后,建立与服务器的连接,并获得与服务器交互的流通道(NetworkStream)
l 通过网络流通道与服务器端的程序进行数据通信。向服务器发送服务器能够识别的以上4种命令请求(注意,命令格式一定语服务器端的命令格式一致),同时也接收服务器发回的命令。客户端能够识别的命令有JOIN(通知当前在线用户有新的用户进入聊天室)、LIST(更新当前在线用户)和QUIT(关闭客户端程序)。客户端程序接收ASCII字符,用“|”分割信息的各个部分,一个信息包含一个命令,一个或多个信息参数,表6-2列出了所有的命令列表。
表6-2 客户端能够识别的命令列表
命令格式 | 说明 |
JOIN|刚刚进入聊天室的用户名| | 该命令是服务器应用程序收到CONN命令后,由服务器自动向当前在线客户端发送的,以此来通知所有在线用户此时有新的用户进入聊天室,客户端程序(当前所有在线用户)收到此命令后,在各自得窗口中显示此用户已进入聊天室,如图6-3所示 |
LIST|在线用户 1| 在线用户 2|在线用户 3|….| | 该命令是服务器程序收到CONN命令(有心的用户进入聊天室)或EXIT命令(有当前在线用户要退出聊天室)后,由服务器自动向当前在线的客户端发送的,以此来通知所有在线用户刷新自己的当前在线用户列表 客户端程序 (当前所有在线用户)收到此命令后,在各自的窗口中刷新当前在线用户列表 |
QUIT| | 该命令是服务器程序收到客户端发送来的EXIT命令后,由服务器自动向该客户端发送的,以 便通知该客户端关闭连接同时关闭客户端程序 客户端程序(发送EXIT命令的用户)收到此命令后,关闭此服务器的连接,并且关闭客户端程序 |
6.3 解决方案
在本节中,将根据第6.2节中的设计方案给出下降应的编码实现,主要包含下面的内容
*服务器端的实现
*客户端的实现
6.3.1 服务器端的实现
服务器端是一个Windows窗体应用程序。首先创建一个Windows窗体应用程序,命名为ChatServer,如图6-8所示。当运行服务器程序时,就会得到如前面的图6-1所示的界面。
图6-8 服务器窗体应用程序
为了使用Sscket对象和Thread对象,在代码文件中加入名字空间System.Net,system.Net.Sockets和System.Thread的引用,在项目ChatServer中将Class1.cs重命名为ChatServer.cs。
在服务器端使用了多线程,每个用户通过一个单独的线程进行连接,当聊天服务器开始运行时,它就启动一个线程等待客户连接(在方法StartListen()中实现)。当接收到一个请求时,服务器立刻启动一个新的县城来处理和该用户端的信息交互(在方法ServerClient()中实现)。这里自定义了一个Client类,它用于保存每个当前在线用户的用户名和与服务器连接Socket对象。当Socket连接一旦建立,就马上将其保存在一个Client对象中,以便让每个用户有自己的Socket,以后可以对不同用户的Socket对象进行操作,实现与客户端的数据交流。程序文件ChatServer.cs的代码实现具体如下:
InitializeComponent();
//
// TODO:Add any constructor code after InitializeComponent call
//
}
///<summary>
/// Clean up any resources being used.
///</summary>
protected override void Dispose(bool disposing)
{
If(disposing)
{
If(components!=null)
{
Components.Dispose();
}
}
Base.Dispose(disposing);
}
#region Windows Forms Desiger generated code
///<summary>
///Required method for Designer support-do not modify
///the contents of this method with the code editor.
///</summary>
private void InitializeComponent()
{
this.label1 = new System.Windows.Forms.Label();
this.label2 = new System.Windows.Forms.Label();
this.texHost = new System.Windows.Forms.TextBox();
this.txtPort = new System.Windows.Forms.TextBox();
this.btnStart = new System.Windows.Forms.Button();
this.btnExit = new System.Windows.Forms.Button();
this.CurUserList = new System.Windows.Forms.ComboBox();
this.label3 = new System.Windows.Forms.Label();
this.IstInfo = new System.Windows.Forms.ListBox();
this.SuspendLayotu();
//
//label1
//
This.label1.Location = new System.Drawing.Point(0,16);
this.label1.Name=”label1”’;
this.label1.TabIndex=0;
this.label1.Text=”主机号”;
this.label1.TextAlign=System.Drawing.ContentAlignment.MiddleCenter;
//
//label2
//
this.label2.Location=new System.Drawing.Point(0,48);
this.label2.Name=”label2”;
this.label2.TabIndex=1;
this.label2.Text=”端口号”;
this.label2.TextAlign=System.Drawing.ContentAlignment.MiddleCenter;
//
//txtHost
//
this.txtHost.Location=new System.Drawing.Point(120,16);
this.txtHost.Name=”txtHost”;
this.txtHost.TabIndex=2;
this.txtHost.Tesxt=””;
//
//txtPort
//
this.txtPort.Location=new System.Drawing.Point(120,48);
this.txtPort.Name=”txtPort”;
this.txtPort.ReadOnly=true;
this.txtPort.TabIndex=3;
this.txtPort.Text=””;
//
//btnStart
//
this.btnStart.Location=new System.Drawing.Point(232,16);
this.btnStart.Name=”btnStart”;
this.btnStart..TabIndex=4;
this.btnStart.Text=”启动”;
this.btnStart.Click+=new System.EventHandler(this.btnStart_Click);
//
//btnExit
//
this.btnExit.Location=new System.Drawing.Point(232,64);
this.btnExit.Name=”btnExit”;
this.btnExit.TabIndex=5;
this.btnExit.Text=”退出”;
//
//CurUserList
//
this. CurUserList.Location=new System.Drawing.Point(112,96);
this. CurUserList.Name=” CurUserList”;
this. CurUserList.Size=new System.Drawing.Size(121,20);
this. CurUserList.TabInde=6;
//
//label3
//
this. label3.Location=new System.Drawing.Point(8,96);
this. label3.Name=” label3”;
this. label3.Size=new System.Drawing.Size(96,23);
this. label3.TabIndex=8;
this. label3.Text=”当前在线用户:”;
//
//lstInfo
//
this.lstInfo.ItemHeight=12;
this. lstInfo .Location=new System.Drawing.Point(0,128);
this. lstInfo.Name=” lstInfo”;
this. lstInfo.Size=new System.Drawing.Size(304,136);
this. lstInfo.TabIndex=9;
//
//Form1
//
this.AutoScaleBaseSize=new System.Drawing.Size(6,14);
this.ClientSize=new System.Drawing.Size(312,273);
this.Controls.Add();
this.Controls.Add();
this.Controls.Add();
this.Controls.Add();
this.Controls.Add();
this.Controls.Add();
this.Controls.Add();
this.Controls.Add();
this.Controls.Add();
this.Name=”Form1”;
this.Text=”ChatServer”;
this.ResumeLayout(false);
}
#endregion
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace ChatSever
{
///<summary>
///Summary description for Form1
///</summary>
Public class ClientSeverForm:System.Windows.Forms.Form
{
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.TextBox txtHost;
private System.Windows.Forms.TextBox txtPort;
private System.Windows.Forms.Button btnStart;
private System.Windows.Forms.Button btnExit;
private System.Windows.Forms.ComboBox CurUserList;
pvivate System.Windows.Forms.ListBox IsInfo;
///<summary>
///Required designer variable
///</summary>
private System.ComponentModel.Container components=null;
//该服务器默认的监听的端口号
static int port=1234;
private TcpListener listener;
private Socket tmpSocket;
//服务器可以支持的最多的客户端的连接数
static int MaxNum=100;
//clients 数组保存当前在线用户的Client对象
static ArrayList clients=new ArrayList();
public ClientSeverForm()
{
//
//Required for Windows Form Designer support
//
///<summary>
///The main entry point for the application.
///</summary>
[STAThread]
static void Main()
{
Application.Run(new ClientSeverForm());
}
在服务器窗体中(如图6—1所示),单击“启动”按钮,进入btunStart_Click处理程序。在btnStart_Click处理程序中,创建了一个服务器套接字并且监听本机IP 地址中的一个指定的端口,同时启动一个线程等待用户连接(在方法StartListen()中实现),具体代码如下:
Private void btnStar_Click(object sender ,system.EvenArgs e)
{
txePort.Text=port.ToString();
try
{
IPAddress ipAdd= IPAddress.Parse(“210.77.26.37”);
txtHost.Text=” 210.77.26.37 ”;
//创建服务器套接字
Listener=new TcpListener(ipAdd,port);
//开始监听服务器端口
Listener.Start();
lstInfo.Item.Add(“服务器已经启动,正在监听”+txtHost.Text+”:”+txtPort.Text);
//启动一个新的线程,执行方法this.StartListen,以便在一个独立的进程中执行确认与客户端连接的操作
Thread thread=new Thread(new ThreadStart(this.StatListen));
Thread.Start();
btnStart.Enabled=false;
}
catch(Exception ex)
{
IstInfo.Items.Add(ex.Message.ToString());
}
}
StartListen()方法是在新的线程中进行的操作,它主要用于当接受到一个客户端请求时,确认与客户端的连接,并且启动一个新的线程来处理和该客户端的信息交互(在方法ServiceClient()中实现),具体代码如下:
Private void startListen()
{
While(true)
{
Try
{
//当接受一个客户端的请求时,确认与客户端的连接
Socket socket=listener.AcceptSocket();
//用tmpSocket保存发出请求的客户端实例
tmpSocket=socket;
if(clients.Count>=MaxNum)
{
tmpSocket.Close();
}
Else
{
//启动一个新的线程,执行方法this.ServiceClient,处理用户相应的请求。
Thread clientService=new Thread(
new ThreadStart(this.ServiceClient));
clientService.Start();
}
}
Catch(Exception ex)
{
IsInfo.Items.Add(ex.Message.ToString());
}
}
}
ServiceClient()方法用与和客户端进行数据通信,包括接受客户端的请求,根据不同的请求命令,执行相应的操作,并将处理结果返回到客户端,此方法完成了服务器的全部工作,具体实现代码如下:
Private void ServiceClient()
{
//定义一个byte数组,用与接收从客户端发送来的数据,每次所能接收的数据包的最大长度为1024个字节。
byte[] buff=new byte[1024];
Socket clientSocket=tmpSocket;
bool keepconnect=true;
//用循环不断地与客户端进行交互,直到客户端发出“EXIT”命令,将keepConnect置为false,退出循环,关闭连接,并中止当前线程。
}
while(keepConnect)
{
//接收收据并存人buff数组中
clientSocket.Receive(buff);
//将字符数组转化为字符串
string clientCommand=System.Text.Encoding.ASCII.GetString(buff);
string[] tokens=clientCommand.Split(new char[]{'|'});
//tokensp[0]中保存了命令标志符(CONN或CHAT或PRIV或EXIT)
if(tokens[0]=="CONN")
{
//此时接收到的命令格式为:命令标志符(CONN)|发送者的用户名|,tokens[1]中保存了发送者的用户名
Client_client=new Client(tokens[1],clientSocker);
clients.Add(_client);
lstInfo.Items.Add(tokens[1]+""+"has joined");
//将刚连接的用户名加入到当前在线用户列表中
CurUserList.Items.Add(tokens[1]);
//对每一个当前在线的用户发送JOIN消息命令和LIST消息命令,以此来更新客户端的当前在线用户列表
for(int i=0;i<clients.Count;i++)
{
Client.client=(Client)client[i];
//向客户端放送JOIN命令,以此来提示有新的客户进入聊天室
SendToClient(client,"JOIN|"+tokens[1]+"|");
Thread.Sleep(100);
string msgUsers="LIST|"+GetUserList();
//向客户端发送LIST命令,以此来更新客户端的当前在线用户列表
SendToClient(client,msgUsers);
}
}
if(tokens[0]=="CHAT")
{
//此时接收到的命令的格式为:命令标志符(CHAT)|发送者的用户名:发送内容|
//向所有当前在线的用户转发此信息
for(int i=0;i<clients.Count;i++)
{
Client client=(Client)clients[i];
//将“发送者的用户名:发送内容”转发给用户
SendToClient(client,tokens[1]);
}
if(token[0]=="PRIV")
{
//此时接收到的命令格式为: 命令标志符(PRIV)|发送者的用户名|接收者的用户名|发送内容|
//tokens[1]中保存了发送者的用户名
string sender=tokens[1];
//tokens[2]中保存了接收者的用户名
string receive=tokens[2];
//tokens[3]中保存了发送的内容
string content=tokens[3];
string message=sender+"send to"+receive+":"+content;
//紧将信息转发给发送者和接收者
for(int i=0;i<clients.Count;i++)
{
Client client==(Client)clients[i];
if(client.Name.CompareTo(tokens[2]==0)
{
SendToClient(client,message);
}
if(client.Name.CompareTo(tokens[1]==0)
{
SendToClient(client,message);
}
}
if(tokens[0]=="EXIT")
{
//此时接收到的命令格式为: 命令标志符("EXIT")|发送者的用户名
//向当前的在线用户发送该用户已离开的信息
for(int i=0;i<clients.Count;i++)
{
Client client==(Client)clients[i];
string message=tokens[1]+"has gone";
SendToClient(client,message);
if(client.Name.CompareTo(tokens[1]==0)
{
//将该用户对应的Client对象从clients数组中删除
clients.RemoveAt(i);
//将该用户名从当前的在线用户列表中删除
CurUserList.Items.Remove(client.Name);
//向客户端发送QUIT命令,以此来关闭客户端程序
message="QUIT";
SendToClient(client,message);
}
}
// 向所有当前在线用户发送LIST命令,以此来更新客户端的当前在线用户列表
for(int i=0;i<clients.Count;i++)
{
Client client=(Client)clients[i];
string message=”LIST|”+GetUserList();
SendToClient(client,message);
}
IsInfo.Items.Add(tokens[1]+”has gone!”);
// 断开与用户的连接
clientSocket.Close();
keepConnect=fasle;
}
}
}
SendToClient()方法实现了向客户端发送命令请求的功能,它利用不同用户保存的Socket对象,向对应的用户发送命令请求,实现代码如下:
private void SendToClient(Client client,string msg)
{
System.Byte[]message=
System.Text.Encoding.ASCII.GetBytes(msg.ToCharArray());
client.clientSocket.Send(message,message.Length,0);
}
GetUserList()方法实现了获取当前在线用户列表的功能,它通过对clients数组的遍历,获取当前在线用户的用户名,用字符串发回,起格式为用户名1|用户名2|…|用户名 n,具体的代码实现如下:
Private string GetUserList()
{
string Rtn=””;
for(int i=0;i<clients.Count;i++)
{
Client client=(Client)clients[i];
Rtn=Rtn+client.Name+”|”;
}
return Rtn;
}
}
我们自定义了一个Clint类,每个当前用户都对应着它的一个实例,它包含的当前用户名和该用户与服务器连接的Socket对象,之所以要设计这个类,是因为这样可以简化ChatServer的工作,具体实现代码如下:
Public class Client
{
String name;
Socket clSocket;
Public Client(string_name,Socket_socket)
{
name=_name;
clSocket=_socket
}
Public string Name
{
get
{
return name;
}
set
{
name=value;
}
}
Public Socket clientSocket
{
get
{
return clSocket;
}
set
{
clSocket=value;
}
}
}
3.2 客户端的实现
客户端是一个Windows窗体应用程序,首先创建一个Windows窗体应用程序,命名ChatClient,如图6-9所示。
图6-9 客户端窗体应用程序
当运行客户端程序时,首先进入如前面图6-2所示的登录界面,输入用户名,单击“登录”按钮后,便进入如图6-3的聊天主窗口,在方窗口中可以向服务器发送信息,同时可以接收从服务器返回的信息。
1. Login.cs文件的实现
在项目ChatClient中一个新的Windows窗体文件——Login.cs,如图6-10所示;
图6-10 用户登录的窗体文件
在Login.cs文件中为了使用Socket对象,在代码文件中加入名字空间System.Net和System.Net.Sockets的引用,它主要实现了创建客户端套接字同时连接到服务器指定端口把用户名和创建的客户端套接字传递给ChatClient窗体,具体实现代码如下:
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Net ;
using System.Net .Sockets ;
namespace CharClient
{
/// <summary>
/// Summary description for Login.
/// </summary>
public class Login : System.Windows.Forms.Form
{
private System.Windows .Forms .Button btnCancel;
private System.Windows .Forms .Button btnLogin;
private System.Windows .Forms .TextBox txtAlias;
private System.Windows .Forms .Label label3;
private System.Windows .Forms .TextBox txtPort;
private System.Windows .Forms .Label label2;
private System.Windows .Forms .TextBox txtHost;
private System.Windows .Forms .Label label1;
/// <summary>
/// Required designer variable
/// </summary>
private System.ComponentModel.Container components = null;
//tcpClient是Login的一个属性,它用于创建客户端套接字
public TopClient tcpClient;
//Alias是Login的一个属性,它向CharClient窗体传送用户名
public string Alias="";
public Login()
{
//
//Required for Windows Form Designer support
//
InitializeComponent();
//
// TODO: Add any constructor code after InitializeComponent call
//
}
/// <summary>
/// Clean up any redources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support-do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.btnCancel=new System.Windows.Forms.Button();
this.btnLogin=new System.Windows.Forms.Button();
this.txtAlias=new System.Windows.Forms.TextBox();
this.label3=new System.Windows.Forms.Label();
this.txtPort=new System.Windows.Forms.TextBox();
this.label2=new System.Windows.Forms.Label();
this.txtHost=new System.Windows.Forms.TextBox();
this.label1=new System.Windows.Forms.Label();
this.SuspendLayout();
//
//btnCancel
//
this.btnCancel.Location=new System.Drawing.Point(160,168);
this.btnCancel.Name="btnCancel";
this.btnCancel.TabIndex=3;
this.btnCancel.Text="取消";
this.btnCancel+=new System.EventHandler(this.btnCancel_Click);
//
//btnLogin
//
this.btnLogin.DialogResult=System.Windows.Forms.DialogResult.OK;
this.btnLogin.Location=new System.Drawing.Point(48,168);
this.btnLogin.Name="btnLogin";
this.btnLogin.TabIndex=2;
this.btnLogin.Text="登录";
this.btnLogin.Click+=new Ststem.EventHander(this.btnLogin_Click);
//
//txtAlias
//
this.txtAlias.Location=new Ststem..Drawing.Point(136,112);
this.txtAlias.Name=”txtAlias”;
this.txtAlias.TabIndex=1;
this.txtAlias.Text=””;
//
//label3
//
this.label3.Location=new System.Drawing.Point(32,112);
this.label3.Name=”label3”;
this.label3.TabIndex=12;
this.label3.Text=”用户名”;
this.label3.TextAlign=System.Drawing.ContentAlignment.MiddleCenter;
//
//txtPort
//
this.txtPort.Location= new System.Drawing.Point(152,64);
this.txtPort.Name=”txtPort”;
this.txtPort.ReadOnly=true;
this.txtPort.Size= new System.Drawing.Point(56,21);
this.txtPort.TabIndex=11;
this.txtPort.Text=”1234”;
//
//label2
//
this.label2.Location=new System.Drawing.Point(32,64);
this.label2.Name=”label2”;
this.label2.TabIndex=10;
this.label2.Text=”端口号”;
this.label2.TextAlign=System.Drawing.ContentAlignment.MiddleCenter;
//
//txtHost
//
this.txtHost.Location=new System.Drawing.Point(136,24);
this.txtHost.Name=”txtHost”;
this.txtHost.TabIndex=9;
this.txtHost.Text=”210.77.26.37”;
//
//label1
//
this.label1.Location=new System.Drawing.Point(32,24);
this.label1.Name=”label1”;
this.label1.TabIndex=8;
this.label1.Text-“服务器地址”;
this.label1.TextAlign=System.Drawing.ContentAlignment.MiddleCenter;
//
//Login
//
this.AutoScaleBaseSize=new System.Drawing.Size(6,14);
this.ClientSize=new System.Drawing.Size(312,221);
this.Controls.Add(this.btnCancel);
this.Controls.Add(this.btnLogin);
this.Controls.Add(this.txtAlias);
this.Controls.Add(this.label3);
this.Controls.Add(this.txtPort);
this.Controls.Add(this.Label2);
this.Controls.Add(this.texHost);
this.Controls.Add(this.Label1);
this.Name=”login”;
this.Text=”Login”;
this.ResumeLayout(false);
}
#endregion
在用户登陆窗口中,当单击“登陆”按钮时,便进入btnLogin_Click 处理程序。在btnLogin_Click处理程序中,对输入的用户名进行简单的判断,并且将正确的用户名保存,同时创建客户端的套接字并且连接到服务器,具体代码实现如下:
Private void btnLogin_Click(object sender,System.EventArgs e);
{
txtAlias.Text=txtAlias.Text.Trim();
if(txtAlias.Text.Length==0)
{
MessageBox.Show(“请输入您的昵称!”,”提示信息”,
MessageBoxButtons.OK,MessageBoxIcon.Exclamation);
txtAlias.Focus();
return;
}
try
{//创建一个客户端套接字,它是Login的一个公共属性,将被传递给ChatClient窗体
tcpClient=new TcpClient();
//向指定的IP地址的服务器发出连接请求
tcpClient.Connect(IPAddress.Parse(txtHost.Text),
Int32.Parse(txtPort.Text);
Alias=txtAlias.Text;
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
}
private void btnCancel_Click(object sender,System.EventArgs e)
{
ChatClient.Login.ActiveForm.Close();
}
}
}
2.ChatClient.cs文件的实现
在ChatClient项目中将Form1.cs重命名为ChatClientForm.cs,为了使用Socket对象和Thread对象,在代码文件中加入名字空间System.Net、System.Net.Sockets和System.Thread的引用。
在ChatClient.cs文件中,首先获得与服务器通信的流通道,在用户登陆后,向服务器发送CONN命令以此说明有新的用户进入聊天室,服务器将返回所有的当前在线用户的呢称,选择不同的人,就可以与他(她)们聊天了,如果选中“悄悄话”复选框,则具有私聊的功能,具体实现代码如下:
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace ChatClient
{
///<summary>
///Summary description for Form1.
///</summary>
public class ChatClientForm:System.Windows.Forms.Form
{
private System.Windows.Forms.TextBox txtSendContent;
private System.Windows.Forms.Button btnSend;
private System.Windows.Forms.ListBox lstUsers;
private System.Windows.Forms.StatusBar status;
private System.Windows.Forms.TextBox txtAlias;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.CheckBox priCheckBox;
private System.Windows.Forms.ListBox lstContent;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Button btnExit;
///<summary>
///Required designer variable.
///<summary>
private System.ComponentModel.Container components=null;
//与服务器的连接
private TcpClient tcpclient;
//与服务器数据交互的流通道
private NetWorkStream Strm;
//用户名
private string UserAlians;
//布尔变量用于判断是否为“私聊”
private bool pirvatemode=false;
public ChatClientForm()
{
//
//Required for Windows Form Designer support
//
InitializeComponent();
//
//TODO:Add any constructor code after InitializeComponent call
//
}
///<summary>
///Clean up any resources being used.
///</summary>
protected override void Dispose(bool disposing)
{
If(disposing)
{
If(components!=null)
{
Components.Dispose();
}
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
///<summary>
///Required method for Designer support-do not modify
///</summary>
Private void InitializeComponent()
{
this.txtSendContent = new System.Windows.Forms.TextBox();
this.btnSend = new System.Windows.Forms.Button();
this.lstUsers = new System.Windows.Forms.ListBox():
this.status = new System.Windows.Forms.StatusBar();
this.txtAlias = new System.Windows.Forms.TextBox();
this.label1 = new System.Windows.Forms.Label();
this.priCheckBox = new System.Windows.Forms.CheckBox();
this.lstContent = new System.Windows.Forms.ListBox();
this.label2 = new System.Windows.Forms.Label();
this.btnExit = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// txtSendContent
//
this.txtSendContent.Location = new System.Drawing.Point(80,280);
this.txtSendContent.Multiline = true;
this.txtSendContent.Name = “txtSendContent”;
this.txtSendContent.Size = new System.Drawing.Size(184,32);
this.txtSendContent.TabIndex = 0;
this.txtSendContent.Text = “”;
//
//btnSend
//
this.btnSend.Location = new System.Drawing.Point(272,272);
this.btnSend.Name = “btnSend”;
this.btnSend.TabIndex = 1;
this.btnSend.Text = “发送”;
this.btnSend.Click+=new System.EventHandler(this.btnSend_Click);
//
//lstUsers
//
this.lstUsers.ItemHeight=12;
this.lstUsers.Location=new System.Drawing.Point(0.240);
this.lstUsers.Name=”lstUsers”;
this.lstUsers.Size=new System.Drawing.Size(64.88);
this.lstUsers.TabIndex=2;
//
//status
//
this.status.Location=new System.Drawing.Point(0.327);
this.status.Name=”status”;
this.status.Size=new System.Drawing.Size(352.22);
this.status.TabIndex=3;
this.status.Text=”status”;
//
//txtAlias
//
this.txtAlias.Location=new System.Drawing.Point(176.8);
this.txtAlias.Name=”txtAlias”;
this.txtAlias.ReadOnly=true;
this.txtAlias.TabIndex=4;
this.txtAlias.Text=””;
//
//label1
//
this.label1.Location=new System.Drawing.Point(24.8);
this.label1.Name=”label1”;
this.label1.TabIndex=5;
this.label1.Text=”呢称:”;
this.label1.TextAlign=System.Drawing.ContentAlignment.MiddleCenter;
//
//priCheckBox
//
this.priCheckBox.Location=new System.Drawing.Point(88.240);
this.priCheckBox.Name=”priCheckBox”;
this.priCheckBox.TabIndex=7;
this.priCheckBox.Text=”悄悄话”;
this.priCheckBox.CheckedChanged+=new System.EventHandler(this.priCheckBox_CheckedChanged);
//lstContent
//
this.lstContent.ItemHeight = 12;
this.lstContent.Location = new Sysem.Drawing.Point(0,40);
this.lstContent.Name = “lstContent”;
this.lstContent.Size = new System.Drawing.Size(344,160);
this.lstContent.TabIndex = 8;
//
//label2
//
this.label2.Location = new System.Drawing.Point(0,208);
this.label2.Name = “label2”;
this.label2.Size = new System.Drawing.Size(120,23);
this.label2.TabIndex = 9;
this.label2.Text = “当前在线用户列表”;
this.label2.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
//
//btnExit
//
this.btnExit. Location
this.btnExit. Name
this.btnExit. Size
this.btnExit. TabIndex
this.btnExit.Click+ = new System.EventHandler(this.btnExit_Click);
//
//ChatClientForm
//
this.AutoScaleBaseSize new System.Drawing.Size(6,14);
this.ClientSize = new System.Drawing.Size(352,349);
this.Controls.Add(this.label2);
this.Controls.Add(this.lstContent);
this.Controls.Add(this.priCheckBox);
this.Controls.Add(this.label1);
this.Controls.Add(this.txtAlias);
this.Controls.Add(this.status);
this.Controls.Add(this.lstUsers);
this.Controls.Add(this.btnSend);
this.Controls.Add(this.txtSendContent);
this.Name = “ChatClientForm”;
this.Text = “ChatClient”;
this.Load+=new System.EventHandler(this.ChatClientForm_Load);
this.ResumeLayout(false);
}
#endregion
/// <summary>
/// The main point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.Run(new ChatClientForm());
}
当加载窗体时,便会进入ChatClientForm_Load处理程序.在ChatClientForm_Load处理程序中
首先显示用户登录窗口,如前面的图6-2示.如果登录成功,那么获取与服务器的连接并得到与服务器数据交互的流通道,
向服务器发送CONN请求命令,同时启动一个新的线程用于响应从服务器发回的信息(在方法ServerResponse()中实现),
具体的实现代码如下:
private void ChatClientForm1_Load(object sender, System.EventArgs e)
{
try
{
Login dlgLogin=new Login();
//显示Login对话框
DialogResult result=new DialogResult();
if(result==DialogResult.OK)
{
//当Login窗口登录成功后,其Alias属性中存在着用户名,将其值赋给UserAlias属性
UserAlias=dlgLogin.Alias;
txtAlias.Text=UserAlias;
//其tcpClient属性中存放着与服务器的连接将其值赋给tcpClient属性
tcpclient=dlgclient.GetStream();
//关闭登录窗口
dlgLogin.Close();
}
else
{
//用户登录窗口中单击了了“取消”按钮
IsUsers.Enabled=false;
txtSendcontent.ReadOnly=true;
btnSend.Enabled=false;
//关闭登录窗口
dlgLogin.Close();
}
//启动一个新的线程,执行方法 this.ServerResponse(),以便来响应从服务器发回的信息
Thread thread =new Thread(new ThreadStart(this.ServerResponse));
Thread.Start();
//向服务器发送“CONN”请求命令,此命令的格式与服务器端的定义格式一致
命令格式为:命令标志符(CONN)|发送者的用户名|
String cmd=”CONN|”+UserAlias+”|”;
/将字符串转化为字符数组
Byte[] outbytes=System.Text.Encoding.ASCII.GetBytes(cmd.ToCharArray());
//利用NetWorkStream的 Write方法发送
Strm.Write(outbytes,0,outbytes.length);
}
Catch(Exception ex)
{
MessageBox.show(ex.Message);
}
}
ServerResponse()方法用于和服务器进行数据通信,主要是接收从服务器发回信息,根据不同的命令,执行相应的操作,具体代码如下:
Private void ServerResponse()
{
//定义一个byte 数组,用于接收从服务器端发送来的数据,每次所能接收的数据
包最大长度为1024字节
Byte[] buff=new byte[1024];
String msg;
Int len ;
Try
{
If (!Strm.CanRead)
return;
//用死循环来不断地与服务器进行交互,直到服务器发出“CONN“命令,则退出循环,关闭连接,并且关闭客户端程序
while(true)
{
//从流中得到数据,并存入到buff字符数组中
len=Strm.Read(buff,0,buff.Length);
//将字符数组转化为字符串
msg=System.Text.Encoding.ASCII.GetString(buff,0,len);
msg.Trim();
string[] tokens=msg.Split(new Char[]{‘|’});
//tokens[0]中保存了命令标志符(LIST或JION 或QUIT)
if(tokens[0]==”LIST”)
{
//此时从服务器返回 的消息格式:命令标志符(LIST)|用户名1|
用户名|2…(所有在线用户)|
status.Text=”OnLine”;
//更新在线用户列表
lstUsers.Items.Clear();
for(int i=1;i<tokens.Length-1;i++)
lstUsers.Items.Add(tokens[i].Trim());
}
if(tokens[0]==”JOIN”)
{
//此时从服务器返回的消息格式:
命令标志符(JOIN)|刚刚登陆的用户名|
lstContent.Items.Add(tokens[1]+” ”+”has enter the chatroom!”);
}
//如果从服务器受到的命令,那么中止与服务器的连接并且直接显示
if(tokens[0]!=”LIST”&&tokens[0]!=”JOIN”&&tokens[0]!=”QUIT”)
{
lstContent.Items.Add(msg);
}
//关闭连接
tcpclient.Close();
关闭客户端程序
this.Close();
}
catch
{
lstContent.Items.Add(“网络放生错误”);
}
}
当选中“悄悄话”复选框时,便会进入 priCheckBox_CheckedChanged 处理程序。在
priCheckBox_CheckedChangee处理程序中主要对privatemode布尔属性进行设置,具体代码如下:
private void priCheckBox_CheckedChange(object sender,System.EventArgs e)
{
If(priCheckBox.Checked)
{
Privatemode=true;
}
Else
{
Privatemode=false;
}
}
当单几“发送”按扭时便进入btnSend_Chick处理程序。在btnSend_Click处理程序中,
如果privatemode布尔属性直为false(说明不是私聊),将CHAT命令发送给服务器;
否则(为私聊),将PRIV 命令发送给服务器,注意命令格式一定与服务器端的命令格式一致,句体代码如下:
Private void btnSend_Click( object sender,System.EventArgs e)
{
Try
{
If(!privatemode)
{
String message=”CHA|”+UserAlias+”:”+txtSendContent.Text+”|”;
txtSendCoutent.Text=””;
txtSendContent.Focus();
Byte[]outbytes=
System.Text.Encoding.AsCII.GetBytes(message.ToCharArray());
Strm.Write(outbytes,0,outbytesLength);
}
Else
}
}
If(IstUsers.SelectedIndex==-1)
{
MessageBox.Show(“请在列表中选择一个用户”,”提示信息”,MessageBoxButtons.OK,MessageIcon.Exclamation);
Return;
}
String receiver=IstUsers.SelectItem.ToString();
//消息的格式是:
命令标志符(PRIV )|发送者的用户名|接受这|的用户名|发送内容|
String message=
“PRIV|”+UserAlias+”|”+ “+receiver+”|”+txtSendContent.Text+”;|”;
txtSendContent.Text=””;
txtSendContent.Focus();
//将字符串转化为字符数组
Byte[] outbytes=
System.Text.Encoding.ASCII.GetBytes(message.ToCharArray());
//利用NetworkStream的Write方法发送
Strm.Write(outbytes,0,outbytes.Length);
}
}
Catch
{
IstContent.Items.Add(“网络发生错误”);
}
}
当点击”离开”按钮时,便进入了btnExit_Click处理程序。在btnExit_Click 处理程序中,将EXIT命令发送给服务器,此命令格式要与服务器端的命令格式一致,具体代码如下:
Private void btnExit_Click(Object sender , System.EventArgs e)
{
String message=”EXIT|”+UserAlias+”|;
//将字符串转化为字符数组
Byte[]outbytes=System.Text.Encoding.ASCII.GetBytes(message.ToCharArray());
//利用NetworkStream的Write方法发送
Strm.Write(outbytes,0,outbytes.Length);
}
}
}