摘 要
即时通信(Instant Message),由于其具有实时性、跨平台性、成本低、效率高等优点而受到广泛的使用。设计并实现一个能够处理多用户进行实时、安全的即时通信系统具有较强的现实意义。即时通信的底层通信是通过SOCKET套接字接口实现的。当前的主流UNIX系统和微软的WINDOWS系统都在内核提供了对SOCKET字接口的支持。使用这个统一的接口,可以编写一个可移植的TCP/IP通信程序。使信息能够在INTERNET上可靠的传输。
本文设计并实现了基于局域网内的简单即时通信系统,系统采用C/S模式,底层通信通过SOCKET套接字接口实现,服务器负责客户端的登录验证,好友信息的保存和心跳报文的发送。客户端采用P2P方式实现消息传递,并能实现文件的传输。本文首先讨论了同步套接字,异步套接字,多线程并发执行任务等;然后阐述了客户端、服务器如何使用XML序列化的消息进行通信。
关键词: 即时通信;文件传输;套接字;TCP协议
2.1 NET开发平台及C#.NET开发语言
.NET框架是Microsoft公司推出的一种全新的开发平台,提供了统一的、面向对象并且可以扩展的编程类库和完善的集成开发环境,大大简化了应用程序的开发过程,并且具有良好的移植性和安全性。
微软为了推行.NET战略,特别为.NET平台设计了一种语言------C#。C#是由C和C++派生而来的一种"简单、流行、面向对象、类型安全"的程序设计语言,其综合了Visual basic的高效率和C++的强大功能,然而更多的人感觉C#更类似JAVA。事实上C#融合了大量的JAVA思想,C#是.NET的关键性语言,它是整个.NET平台的基础。与C#相比,.NET所支持的其它语言显然是配角,包括VC++.NET在内。但是微软并没有打算放弃VC++.NET,相反,微软对VC++.NET有着另一番独特的打算,VC++.NET的定位与C#不完全重合,VC++.NET应用范围仍强于C#,这一点无论对微软公司还是软件业应用现状都非常重要。
2.1 TCP协议
2.2.1 TCP/IP网络协议
计算机网络中已经形成的网络体系结构主要有两个:OSI参考模型和TCP/IP参考模型。TCP/IP参考模型是因特网(Internet)的基础。和OSI的7层协议相比,TCP/IP协议只有4个层次。通常说的TCP/IP是一组协议的总称,TCP/IP实际上是一个协议族,包括100多个相互关联的协议,其中IP(Internet Protocol, 网际协议)是网络层最主要的协议;TCP(Transmission Control Protocol,传输控制协议)和UDP(User Datagram Protocol,用户数据报协议是传输层中最主要的协议),一般认为IP、TCP、UDP是最根本的三种协议,是其他协议的基础。
2.2.2 TCP------传输控制协议
使用TCP协议的应用层协议包括HTTP、FTP、SMTP和Telnet等。
TCP要求在发送数据之前必须打开连接。服务器应用程序必须执行一个称作被动打开(passive open)的操作,以利用一个已知的端口号创建一个链接,这是,服务器并不是对网络进行呼叫,而是侦听并等待引入的请求。客户应用程序必须执行一个主动打开(active open),为此,它向服务器应用程序发送一个同步序列号(SYN)以标识连接。客户应用程序可以将动态端口号作为本地端口使用。服务器必须向客户发送一个确认(ACK)以及服务器的序列号(SYN)。随后,客户回复一个ACK,这样就建立了链接。
2.3 套接字
套接字这个术语并没有定义某个协议:它具有两层含义,但两者都与一个协议相关。第一个含义是套接字编程API,它最初由伯克利大学为BSD UNIX而创建。BSD套接字在经过修改后被用作Windows环境的编程接口(并且被命名为WinSock)。WinSock API被包装在System.Net.sockets命名空间的.NET类中。Windows Sockets 是一个独立于协议的编程接口,用于编写网络应用程序。
套接字的第二层含义表示一个用于在进程间进行通信的终端。在TCP/IP中,每个终端都与一个IP地址和一个端口号绑定。我们必须对流式套接字和数据报套接字这两种类型进行区分。流失套接字用TCP/IP协议来使用面向连接的通信;另一方面,数据报套接字用UDP/IP来使用无连接通信。
2.4 流
2.4.1 流的基本概念
流的概念已经存在很长时间了。流是一个用于传输数据的对象。数据的传输有两个方向:
- 如果数据从外部源传输到程序中,这就是读取流。
- 如果数据从程序传输到外部源,这就是写入流。
外部源常常是一个文件,但也不完全都是文件,它还可以是:
- 网络,使用一定的网络协议与网络上其它计算机或终端交换数据。
- 一个指定的管道。
- 一块内存区域。
2.4.2 NET中的流
在这些情况中,微软提供了一个.NET基类System.IO.MemoryStream来读写内存数据使用System.Net.Sockets.NetworkStream处理网络数据。读写管道没有相应的流类,但有一个常见的流类System.IO.Stream,如果要编写一个这样的类,可以从这个基类继承。流对外部数据源不做任何假定。外部源还可以是代码中的一个变量,使用流在变量之间传输数据的技术是一个非常有用的技巧,可以在数据类型之间转换。
在网络编程中我们经常会使用到网络中的流对象:NetworkStream。它实现了.NET中标准的Stream机制,即可以使用NetworkStream通讯网络套接字用标准的流操作进行网络数据的读写。它提供以下的功能:
-
一个统一的从网络中读取数据的方法
-
与其他的.NET流兼容,这样你可以很容易地移植程序。
2.5 同步、异步、阻塞和非阻塞
同步(synchronous):所谓同步方式,就是发送方发送数据包以后,不等接受方响应,就接着发送下一个数据包。
异步(asynchronous):异步方式就是当发送方发送一个数据包以后,一直等到接受方响应后,才接着发送下一个数据包。
阻塞(Block):指执行此套接字的网络调用时,直到调用成功才返回,否则此套节字就一直阻塞在网络调用上,比如调用StreamReader 类的Readlin ( )方法读取网络缓冲区中的数据,如果调用的时候没有数据到达,那么此Readlin ( )方法将一直挂在调用上,直到读到一些数据,此函数调用才返回
非阻塞(Unblock):指在执行此套接字的网络调用时,不管是否执行成功,都立即返回。同样调用StreamReader 类的Readlin ( )方法读取网络缓冲区中数据,不管是否读到数据都立即返回,而不会一直挂在此函数调用上。
在Windows网络通信软件开发中,最为常用的方法就是异步非阻塞套接字。平常所说的C/S(客户端/服务器)结构的软件采用的方式就是异步非阻塞模式的。
2.6 C/S模型
客户机/服务器模型,又称为Client/Server模型,简称C/S架构。C/S计算技术在信息产业当中占有重要的地位。
这种客户机/服务器模型是一种非对称式编程模式。该模式的基本思想是把集中在一起的应用划分成为功能不同的两个部分,分别在不同的计算机上运行,通过它们之间的分工合作来实现一个完整的功能。对于这种模式而言其中一部分需要作为服务器,用来响应并为客户提供固定的服务;另一部分则作为客户机程序用来向服务器提出请求或要求某种服务。
在此"服务器"是指能在网络上提供服务的任何程序。服务器接受网络上的请求,完成服务后将结果返回给申请者。对于简单的服务,把每个请求用一个IP数据报发给服务器,服务器用另一个数据报返回响应。
客户机和服务器都是独立的计算机。当一台连入网络的计算机向其他计算机提供各种网络服务(如数据、文件的共享等)时,它就被叫做服务器。而那些用于访问服务器资料的计算机则被叫做客户机。严格说来,客户机/服务器模型并不是从物理分布的角度来定义,它所体现的是一种网络数据访问的实现方式。采用这种结构的系统目前应用非常广泛。如宾馆、酒店的客房登记、结算系统,超市的POS系统,银行、邮电的网络系统等。
2.7 即时通信协议
协议是一系列的步骤,它包括双方或者多方,设计它的目的是要完成一项任务。即时通信协议,参与的双方或者多方是即时通信的实体。协议必须是双方或者多方参与的,一方单独完成的就不算协议。这样在协议动作的过程中,双方必须交换信息,包括控制信息、状态信息等等。这些信息的格式必须是协议参与方同意并且遵循的。好的协议要求清楚,完整,每一步都必须有明确的定义,并且不会引起误解;对每种可能的情况必须规定具体的动作。
有许多的 IM 系统,如 AOL IM、Yahoo IM 和 MSN IM,它们使用了不同的技术,而且它们互不兼容。为了创建即时通信的统一标准,人们经过了多次尝试:IETF 的对话初始协议(SIP)和 即时通信对话初始协议和表示扩展协议(SIMPLE)、应用交换协议(APEX)、显示和即时通信协议(PRIM)及基于 XML 且开放的可扩展通信和表示协议(XMPP)协议(常称为 Jabber 协议)。人们多次努力,试图统一各大主要 IM 供应商的标准(AOL、Yahoo 及 Microsoft),但无一成功,且每一种 IM 仍然继续使用自己所拥有的协议。
本系统目的在于实现一个简单的即时通信过程,没有必要采用通用的比较复杂的即时通信协议,因此使用了简单定义的XML标记定义来规范即时通信的各种网络信息,在网络中传输序列化的XML语言。
4.1 使用XML定义的即时通信协议
4.1.1 信息结构MESSAGE.CS&UMESSAGE.CS
这两个C#类定义了包括服务器信息,状态信息,注册信息,登录信息,聊天信息或者请求文件传输信息的函数,服务器和客户端通过将它们实例化和序列化再转换成流在网络上进行传输。UMESSAGE.CS主要代码如下:
[Serializable]
public class UMessage
{
public UMessage(){ }
private string _nickname;
private string _password;
private string _accounts;
private string _email;
private int _info;//表示注册或者登录信息,客户端信息0为注册,1为登录;服务器返回信息0为用户已存在,1为注册成功,2为服务器未知错误,3为CLIENT在线检查,10为登录失败,11为登录成功
private Friend[] _friend;
private int _fn;
private string _fg;
public string Nickname
{
get { return _nickname; }
set { _nickname = value; }
}
public string Password
{
get { return _password; }
set { _password = value; }
}
public string Accounts
{
get { return _accounts; }
set { _accounts = value; }
}
public string Email
{
get { return _email; }
set { _email = value; }
}
public int Info
{
get { return _info; }
set { _info = value; }
}
public Friend[] Fri
{
get { return _friend; }
set { _friend = value; }
}
public int Fn
{
get { return _fn; }
set { _fn = value; }
}
public string Fg
{
get { return _fg; }
set { _fg = value; }
}
}
由于MESSAGE.CS与UMESSAGE.CS类似,在此不再详述。
服务器和客户端都可以通过相同的代码对UMESSAGE赋值,再通过XmlSerializer方法进行将UMESSAGE序列化为XML文档,最后将XML文档转化为网络流进行传输。代码如下:
#region 将登录信息转为UMessage
private void Traslator()
{
_message.Accounts=this.TextBox1.Text;
_message.Nickname="";
_message.Password=this.TextBox2.Text;
_message.Email="";
_message.Info=1;
_message.Fri=null;
}
#endregion
4.1.2 数据结构FriendStruct
服务器如果保存和传递用户的好友信息是难点之一。数据库的设计和信息的传递辨别都是比较难实现的。
在数据库方面,每个用户拥有各自的好友分组信息(UserFav),分组中间使用","分隔,在TCP_FriendInfo表中则分别保存了用户ID和好友ID,使用一个INT字段保存分组信息。数据库以用户ID为标准对好友ID和分组信息进行内连接查询,就可以得到基本的好友信息了。代码如下:
select * from TCP_UserInfo join TCP_FriendInfo on TCP_FriendInfo.UserID='" + uid + "' and TCP_UserInfo.UserID=TCP_FriendInfo.FriendID
在好友信息的传输方面,首先定义一个FriendStruct数据结构(当然也可以用枚举完成)如下:
using System;
using System.Collections.Generic;
using System.Text;
namespace TCP
{
public class FriendStruct
{
public struct FileInfo
{
public int filere;//接收和拒绝信息,1为接收,2为拒绝,3为取消
public string filename;
public long filelength;
}
}
public struct Friend
{
public string account;
public string nickname;
public string IP;
public string status;
public string fg;//好友分组
}
}
在MESSAGE.CS或者UMESSAGE.CS中,我们则定义了FriendStruct的数组。在C#中使用DATAREADER语句可以逐句读取数据库查询的结果,再依次将结果赋值FriendStruct数组元素,就得到了便于发送和读取的存放好友信息的数组。赋值代码如下:
while (getf.Read()) //getf即是以上的数据库查询的datareader语句
{
ff[i].account=getf["UserAccount"].ToString();
ff[i].IP = getf["UserIP"].ToString();
ff[i].nickname = getf["UserNickname"].ToString();
ff[i].status = getf["UserOnline"].ToString();
ff[i].fg = getf["FriendGroup"].ToString();
i++;
}
getf.Close();
4.2 数据库连接类
实现一个快捷简单的数据库连接的相关代码是非常有必要的。实现的途径也多种多样,鉴于安全性和复杂性的需求不同,实现方法有简有繁。本设计使用了一个简单的类(UserData.CS)实现了简单快捷的数据库连接和读取。主要代码如下:
public static SqlConnection connStr = new SqlConnection("Server=D96B85DD938A465.;uid=sa;pwd=change;database=TCPDB");
public static SqlDataReader SqlReader(string sql, SqlConnection connstr)
{
SqlDataReader sqldr = null;
SqlCommand cmd = new SqlCommand(sql, connstr);
if (cmd.Connection.State.ToString() == "Closed") cmd.Connection.Open();
try
{
sqldr = cmd.ExecuteReader();
}
catch (Exception e)
{
if (e != null) sqldr = null;
}
return sqldr;
}
//数据库操作连接
public static string SqlCmd(string sql, SqlConnection connstr)
{
string errorstr = null;
SqlCommand sqlcmd = new SqlCommand(sql, connstr);
if (sqlcmd.Connection.State.ToString() == "Open") sqlcmd.Connection.Close();
sqlcmd.Connection.Open();
try
{
sqlcmd.ExecuteNonQuery();
}
catch (Exception e)
{
if (e != null) errorstr = e.ToString();
}
sqlcmd.Connection.Close();
return errorstr;
}
在UserData.CS的基础上,主程序可以更方便地实现数据库连接操作,对数据库进行读写和更新,在此不再详述。
4.3 服务器端
服务器端的界面设计是基于便于测试的目的而实现的。如下图:
图4 服务器端界面
4.3.1 同步套接字网络监听
基于同步套接字的网络监听器对服务器来说并不是最好的解决方案,但是仍然可行并且实现简单。主要代码如下:
开启监听端口:
public void Serve()
{
int port = 8888;
ServerIPEP = new IPEndPoint(IPAddress.Any, port);
s = new Socket(ServerIPEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
s.Bind((EndPoint)ServerIPEP);
s.Listen(10);
alSock = new ArrayList();
以下代码读取连入的连接,依次将连接加入可变长数组alsock,并且读取传入的信息,进行反串行化:
while (true)
{
try
{
uc = s.Accept();
alSock.Add(uc);
this.tb_states.AppendText(System.Convert.ToString(uc));
byte[] data = new byte[2048];
int rect = uc.Receive(data);
byte[] chat = new byte[rect];
Buffer.BlockCopy(data, 0, chat, 0, rect);
UMessage umessage = (UMessage)_translator.Deserialize(new MemoryStream(chat));
int info = umessage.Info;
对反串行化后的信息进行处理,通过info参数辨认客户端行为(注册或者登录),对注册的信息进行数据库查询,注册信息可插入,则将用户信息插入数据库,否则返回客户端"注册出错"的信息:
#region 处理用户注册信息
if (info==0)//分辨出用户发送的是注册信息
{
string Accounts = umessage.Accounts;
SqlDataReader usdr = FPara.SqlReader("select * from TCP_UserInfo where UserAccount='" + Accounts + "'", FPara.connStr);
if (usdr != null)
{
if (usdr.Read())
{
#region 此处写入返回注册失败的代码
Socket sc = (Socket)alSock[alSock.IndexOf(uc, 0)];
sc.Send(chat);
#endregion
}
else
{
#region 此处写入插入数据库用户注册信息的代码
Stream ms = new MemoryStream();
Socket sc = (Socket)alSock[alSock.IndexOf(uc, 0)];
if (FPara.SqlCmd("insert into TCP_UserInfo (UserAccount,UserNickname,UserEmail,JoinDate,UserIP,UserPassword) values('" + umessage.Accounts + "','" + umessage.Nickname + "','" + umessage.Email + "','" + System.DateTime.Now.ToString() + "','" + ((IPEndPoint)uc.RemoteEndPoint).Address.ToString() + "','" + umessage.Password + "')", FPara.connStr) == null)
{
umessage.Info = 1;
_translator.Serialize(ms, umessage);
byte[] d = new byte[ms.Length];
ms.Seek(0, SeekOrigin.Begin);
ms.Read(d, 0, d.Length);
sc.Send(d);
}
else
{
umessage.Info = 2;
_translator.Serialize(ms, umessage);
byte[] d = new byte[ms.Length];
ms.Seek(0, SeekOrigin.Begin);
ms.Read(d, 0, d.Length);
sc.Send(d);
}
#endregion
}
usdr.Close();
}
}
#endregion
如果发现用户发送的是登录信息,就根据登录信息中的用户名和密码判断是否存在用户,密码是否正确,成功后再查询出用户的好友信息并且赋值给FriendStruct,再将信息返回给客户端:
#region 处理用户登录信息
else if (info == 1)//分辨出用户发送的是登录信息
{
string Accounts = umessage.Accounts;
string Password = umessage.Password;
SqlDataReader usdr = FPara.SqlReader("select * from TCP_UserInfo where UserAccount='" + Accounts + "' and UserPassword='"+Password+"'", FPara.connStr);
if (usdr != null)
{
if (usdr.Read())
{
string uid=usdr["UserID"].ToString();
umessage.Fg = usdr["UserFav"].ToString();
usdr.Close();
SqlDataAdapter sdr = new SqlDataAdapter("select * from TCP_UserInfo join TCP_FriendInfo on TCP_FriendInfo.UserID='" + uid + "' and TCP_UserInfo.UserID=TCP_FriendInfo.FriendID", FPara.connStr);
DataSet ds = new DataSet();
sdr.Fill(ds, "find");
int xxx=ds.Tables["find"].Rows.Count;
FPara.SqlCmd("update TCP_UserInfo set UserIP='" + ((IPEndPoint)uc.RemoteEndPoint).Address.ToString() + "' , UserOnline=1 where UserAccount='" + Accounts + "'", FPara.connStr);
ff=new Friend[xxx];
int i=0;
SqlDataReader getf = FPara.SqlReader("select * from TCP_UserInfo join TCP_FriendInfo on TCP_FriendInfo.UserID='" + uid + "' and TCP_UserInfo.UserID=TCP_FriendInfo.FriendID", FPara.connStr);
while (getf.Read())
{
ff[i].account=getf["UserAccount"].ToString();
ff[i].IP = getf["UserIP"].ToString();
ff[i].nickname = getf["UserNickname"].ToString();
ff[i].status = getf["UserOnline"].ToString();
ff[i].fg = getf["FriendGroup"].ToString();
i++;
}
getf.Close();
#region 此处写入登录成功代码
Stream ms = new MemoryStream();
Socket sc = (Socket)alSock[alSock.IndexOf(uc, 0)];
this.lb_users.Items.Add(alSock.IndexOf(uc).ToString());
umessage.Info = 11;
umessage.Fri = ff;
umessage.Fn = xxx;
_translator.Serialize(ms, umessage);
byte[] d = new byte[ms.Length];
ms.Seek(0, SeekOrigin.Begin);
ms.Read(d, 0, d.Length);
sc.Send(d);
//在tb_status中写入服务器返回给客户端的代码便于测试观察
this.tb_states.AppendText(System.Text.Encoding.Default.GetString(d));
#endregion
}
else
{
usdr.Close();
#region 此处写入登录失败代码
Stream ms = new MemoryStream();
Socket sc = (Socket)alSock[alSock.IndexOf(uc, 0)];
umessage.Info = 10;
_translator.Serialize(ms, umessage);
byte[] d = new byte[ms.Length];
ms.Seek(0, SeekOrigin.Begin);
ms.Read(d, 0, d.Length);
sc.Send(d);
#endregion
}
}
}
#endregion
Tb_states是个用于监视SOCKET传入信息的文本框,便于观察和测试相关信息:
this.tb_states.AppendText("[" + uc.RemoteEndPoint.ToString() + "]" + System.Text.Encoding.Default.GetString(chat));
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
}
以上代码也包含了对客户端的请求信息的判断和对客户端返回信息的生成和传输。
4.3.2 多线程
对于服务器来说,多线程是必不可少的,否则它将无法处理不断请求的新连接。C#的System.Threading提供了多线程编程的支持。本设计实现代码如下:
this.th = new Thread(new ThreadStart(Serve));//新建一个用于监听的线程
th.Start();//打开新线程
不仅仅是服务器,基于P2P模式聊天的客户端也必须支持多线程运行,实现代码与之类似,在客户端设计说明中将不再叙述。
4.3.3 计时器
计时器用于实现心跳报文的功能,服务器在启动以后就开始计时,每隔一定时间就向所有连入的客户端发送信息,核心代码如下:
//用计时器检查客户端是否掉线
System.Timers.Timer aTimer = new System.Timers.Timer();
aTimer.Elapsed += new ElapsedEventHandler(CheckStatus);
// 设置引发时间的时间间隔 此处设置为5秒(5000毫秒)
aTimer.Interval = 5000;
aTimer.Enabled = true;
CheckStatus就是用于向客户端发送检查信息的方法,它会向遍历连入的客户端(alSock),然后依次向客户端发送信息,如果发现客户端没有响应,就会如果发现对方无回应,则关闭相应的SOCKET,并更新数据库的用户在线状态,同时向该用户的所有好友发送用户已下线的通知。
图5 注册界面
图6 登录、聊天、文件传输界面
4.4.1 同步套接字客户端
客户端发起同步套接字连接,并传送登录或者注册信息,由于两者方式类似,这里仅列出用户登录的代码:
#region 发送服务器登录信息,并接收服务器反馈信息
public void Client()
{
建立SOCKET发送信息:
try
{
IPEndPoint ServerIPEP = new IPEndPoint(IPAddress.Parse("222.18.170.16"),8888);
c = new Socket(ServerIPEP.AddressFamily,SocketType.Stream,ProtocolType.Tcp);
c.Connect((EndPoint)ServerIPEP);
s = new MemoryStream();
_translator.Serialize(s,_message);
byte[] d=new byte[s.Length];
s.Seek(0, SeekOrigin.Begin);
s.Read(d, 0, d.Length);
int i = c.Send(d, 0, d.Length, SocketFlags.None);
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
以下代码读取了服务器返回给客户端的信息(注册和登录的成功与失败),如果返回了登录成功的信息,还会读取服务器给出的FriendStruct结构以得到用户的好友信息:
#region 接收反馈信息
byte[] data = new byte[2048];
while(true)
{
int rect = c.Receive(data);
byte[] chat = new byte[rect];
Buffer.BlockCopy(data,0,chat,0,rect);
UMessage bumessage = (UMessage)_translator.Deserialize(new MemoryStream(chat));
string[] fg;
string _fg=bumessage.Fg;
if(bumessage.Info==3)
{
}
else if(bumessage.Info==11)
{
fg=_fg.Split(',');
int xxx=bumessage.Fn;
ff=bumessage.Fri;
for(int i=0;i<xxx;i++)
{
string[] ems=new string[5];
ems[0]=ff[i].account;
ems[1]=ff[i].nickname;
ems[2]=fg[int.Parse(ff[i].fg)];
ems[3]=ff[i].IP;
ems[4]=ff[i].status;
ListViewItem item = new ListViewItem(ems);
this.listView1.Items.Add(item);
}
CSERVER是一个用于开启监听P2P信息的方法,客户端在登录成功以后就会立刻开启监听器,才能够实现与其它客户端的聊天:
th = new Thread(new ThreadStart(CServer));//新建一个用于监听其它客户端信息的线程
th.Start();//打开新线程
MessageBox.Show(bumessage.Accounts+"登录成功!");
this.Button1.Enabled=false;
this.Button3.Enabled=false;
}
else if (bumessage.Info==2)
{
MessageBox.Show("服务器未知错误");
}
else
{MessageBox.Show(bumessage.Info.ToString());}
}
#endregion
}
#endregion
图7 登录成功后的客户端界面
客户端之间的聊天同样使用了序列化的XML文档,用户在登录成功后就会启动一个新的监听器去监听其它客户端传入的聊天信息并且进行判断再将其它用户的聊天信息显示在界面上。这里也不再阐述代码。
4.4.2 采用异步套接字的文件传输
文件传输是通过一个类库实现的。由于文件传输的代码实现复杂,通过类库可以大量的简化代码,使主程序简洁易懂。类库Infinity.Networking包括了ClientBase.cs,ClientInfo.cs,Delegates.cs,INPClient.cs,INPServer.cs,ClientBase.cs定义了基础的文件发送函数,INPClient.cs则仅包含初始化文件发送的函数;ServerBase.cs和INPServer.cs则是反之亦然。核心代码如下:
ClientBase.cs:这个类实现了套接字的开启和数据的传输
using System;
using System.Net;
using System.Net.Sockets;
namespace Infinity.Networking
{
/// <summary>
/// ClientBase摘要.
/// </summary>
public class ClientBase
{
private const int BUFFERSIZE = 4*1024;
private int _port;
private string _serverIP;
private Socket _mainSoc;
private ClientInfo _info;
private AsyncCallback _dataRecievedCallback;//异步回调方法
public event NetworkEventHandler DataRecieved;//定义一个事件:接收到数据时引发事件
public ClientBase(string serverIP,int port)
{
_serverIP = serverIP;
_port = 11000;
_mainSoc = new Socket(
AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
_info = new ClientInfo(
_mainSoc,
new byte[BUFFERSIZE]);//ClientInfo包含了建立的套接字和套接字读取的BYTE大小
_dataRecievedCallback = new AsyncCallback(OnDataRecieved);//异步回调
}
// 可重写为其它超类实现更强大的功能,例如断点续传
public virtual void OnDataRecieved(byte[] data)
{
if (DataRecieved != null)
{
DataRecieved(this,
new NetworkEventArgs(_info));
}
}
public void Send(byte[] data)
{
_mainSoc.Send(data);//发送数据
}
public void Connect()//建立与远程主机的连接
{
_mainSoc.Connect(
new IPEndPoint(
IPAddress.Parse(_serverIP),
_port));
}
public void Disconnect()//关闭连接
{
if (_mainSoc.Connected)
_mainSoc.Shutdown(SocketShutdown.Both);
}
public void WaitForData()
{
// 异步接收数据
_mainSoc.BeginReceive(_info.Buffer,0,_info.Buffer.Length,
SocketFlags.None,
_dataRecievedCallback,null);
}
private void OnDataRecieved(IAsyncResult ar)
{
// 垃圾回收
GC.Collect();
int byteCount = 0;
byteCount = _mainSoc.EndReceive(ar);
if (byteCount == 0)
{
// 服务器断开连接.
}
else
{
OnDataRecieved(_info.Buffer);//接收到了数据
WaitForData();
}
}
}
}
INPClient.cs 派生类INPClinet:
using System;
using System.IO;
namespace Infinity.Networking
{
/// <summary>
/// INPClient的摘要.
/// </summary>
public class INPClient : ClientBase
{
public INPClient(string serverIP,int port) : base(serverIP,port)
{}
public void SendFile(string fileName)//发送文件类,开启一个文件流,将文件流依次读入,再使用CLIENTBASE类中的数据发送方法进行发送
{
FileStream fs = new FileStream(
fileName,FileMode.Open);//根据传入的参数打开文件
byte[] im = new byte[fs.Length];//根据文件长度定义一个BYTE
fs.Read(im,0,im.Length);//将文件流中读取字节块写入相应缓冲区
base.Send(im);//使用基类(CLIENTBASE)的数据发送方法进行文件传送
}
}
}