ASP.NET基于TCP协议的简单即时通信软件的设计与实现

news2024/11/26 14:43:08

摘  要

即时通信(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  流的基本概念

流的概念已经存在很长时间了。流是一个用于传输数据的对象。数据的传输有两个方向:

  1. 如果数据从外部源传输到程序中,这就是读取流。
  2. 如果数据从程序传输到外部源,这就是写入流。

外部源常常是一个文件,但也不完全都是文件,它还可以是:

  1. 网络,使用一定的网络协议与网络上其它计算机或终端交换数据。
  2. 一个指定的管道。
  3. 一块内存区域。

​​​​​​​        2.4.2  NET中的流

在这些情况中,微软提供了一个.NET基类System.IO.MemoryStream来读写内存数据使用System.Net.Sockets.NetworkStream处理网络数据。读写管道没有相应的流类,但有一个常见的流类System.IO.Stream,如果要编写一个这样的类,可以从这个基类继承。流对外部数据源不做任何假定。外部源还可以是代码中的一个变量,使用流在变量之间传输数据的技术是一个非常有用的技巧,可以在数据类型之间转换。

在网络编程中我们经常会使用到网络中的流对象:NetworkStream。它实现了.NET中标准的Stream机制,即可以使用NetworkStream通讯网络套接字用标准的流操作进行网络数据的读写。它提供以下的功能:

        1. 一个统一的从网络中读取数据的方法

        2. 与其他的.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)的数据发送方法进行文件传送

  }

  }

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1604339.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2W,3KVDC隔离 定电压输入,稳压单、双路输出DC-DC模块电源——TPI-2W 系列

TPI-2W系列产品是专门针对PCB上需要与输入电源隔离的电源应用场合而设计的。该产品适用于&#xff1a;1&#xff09;输入电源的电压变化≤5%&#xff1b;2&#xff09;输入输出之间要求隔离电压≥3000VDC&#xff1b;3&#xff09;对输出电压稳定和输出纹波噪声要求高.

linux 安装openjdk-1.8

安装命令 yum install java-1.8.0-openjdk-1.8.0.262.b10-1.el7.x86_64查看安装路径 find / -name java 默认的安装路径 /usr/lib/jvm 查看到jre 以及java-1.8.0-openjdk-1.8.0.262.b10-1.el7.x86_64 配置环境变量 vim /etc/profile 添加的内容 export JAVA_HOME/usr/li…

网络工程师----第三天

HDLC帧格式及控制手段&#xff1a; ipv4到ipv6的过渡技术&#xff1a; 1、双栈技术&#xff1a;主机或路由器同时装有IPV4 和 IPV6两个协议栈&#xff0c;因此&#xff0c;主机既能和IPV4通信&#xff0c;也能和IPv6网络通信。IPv6和IPv4是功能相近的网络层协议&#xff0c;两者…

爱普生计时设备AUTOMOTIVE RA8900CE DTCXO RTC

主要特点出场已校准带有DTCXO的RTC&#xff0c;并且内部集成晶体单元高精度: 3.4 ppm 40 to 85 C(9 s/月.)时钟输出:1 Hz.1024 Hz.32.768 kHzI 2 C Interface: Fast mode (400 kHz)The l2C-Bus is a trademark ofNXP Semiconductors供电电压: 2.5-5.5 V(main),1.6-5.5 V(备份电…

SQL优化——统计信息

文章目录 1、统计信息1.1、表的统计信息1.2、列的统计信息1.3、索引的统计信息 2、统计信息重要参数设置3、检查统计信息是否过期4、扩展统计信息5、动态采样6、定制统计信息收集策略 只有大表才会产生性能问题&#xff0c;那么怎么才能让优化器知道某个表多大呢&#xff1f;这…

果园系统养殖游戏喂养偷菜种植浇水养成小程序

装扮 通过购买装扮场景切换不同的农场风格 土地升级 通过特定的材料对土地和房屋进行升级 日志 记录道具的使用数量及金币农作物的收入情况 幸运转盘 可用金币进行抽奖 宝箱开启 获得宝箱后可以通过金币开启 每日签到 每日签到获得奖励 系统公告 可以第一时间知道游戏的更新和…

Linux进阶--文本处理grep、sed、awk命令

目录 一、grep &#xff08;1&#xff09;用文件查找 二、正则表达式 三、sed命令 四、awk命令 grep、sed、awk可以称作linux里的三驾马车 一、grep grep&#xff1a;一种强大的文本搜索工具&#xff0c;它能使用正则表达式匹配模式搜 索文本&#xff0c;并把匹配的行打…

智能电视/盒子长文字输入困难?手把手教您解决这个难题!(电视盒子跨屏输入/打字,亲测有效!)

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 解决方案 📒📝 方法一📝 方法二🎈 获取方式 🎈⚓️ 相关链接 ⚓️📖 介绍 📖 在使用智能电视/电视盒子的时候,会遇到这样一个场景:需要输入一个很长的网址,或者是想要粘贴一段很长的文字。如何使用遥控器要完成…

【C语言】qsort()函数排序及其模拟实现,万物皆可排!

&#x1f525;博客主页&#x1f525;&#xff1a;【 坊钰_CSDN博客 】 欢迎各位点赞&#x1f44d;评论✍收藏⭐ 目录 1. 函数介绍 2. qsort举例排列整型变量 3. qsort举例排列结构型变量 3.1 按名字排序 3.1.1 srtcmp函数 3.2 按年龄排序 4. qsort函数模拟实现(采用冒泡的…

python怎么连接oracle

一&#xff1a;弄清版本&#xff0c;最重要&#xff01;&#xff01;&#xff01; 首先安装配置时&#xff0c;必须把握一个点&#xff0c;就是版本一致&#xff01;包括&#xff1a;系统版本&#xff0c;python版本&#xff0c;oracle客户端的版本&#xff0c;cx_Oracle的版本…

工作流 jbpm概述

文章目录 1 工作流概述2 jBPM概述3 jBPM开发环境搭建及其配置3.1 准备工作3.2 搭建jBPM开发环境3.3 加入jar包 总结 1 工作流概述 工作流&#xff08;Workflow&#xff09;&#xff0c;就是“业务过程的部分或整体在计算机应用环境下的自动化”&#xff0c;它主要解决的是“使…

STM32 CAN过滤器细节

STM32 CAN过滤器细节 简介 每组筛选器包含2个32位的寄存器&#xff0c;分别为CAN_FxR1和CAN_FxR2&#xff0c;它们用来存储要筛选的ID或掩码 四种模式 模式说明32位掩码模式CAN_FxR1存储ID&#xff0c; CAN_FxR2存储哪个位必须要与CAN_FxR1中的ID一致 &#xff0c; 2个寄存器…

Linux网络配置和操作命令

网络配置命令 Linux系统中最常用的网络配置命令包括 ifconfig route 其中 ifconfig 用来查看和配置网络接口 通常是网卡 信息 包括网络接口设备的 IP 地址 掩码等 route 用来管理 Linux系统内核中的路由表 它最大的用途就是用来设定静态的路由表项 通常是在系统用 ifconfig 配…

内置管线升级到SBP,如何复用之前打包的AssetBundle

1&#xff09;内置管线升级到SBP&#xff0c;如何复用之前打包的AssetBundle 2&#xff09;安卓真机&#xff0c;在Unity 2021.3.31版本下Buffer数据异常 3&#xff09;URP里CullResults.CreateSharedRendererScene下面的消耗 4&#xff09;移动端是否支持曲面细分着色 这是第3…

Element——组件

element官网 https://element.eleme.cn/#/zh-CN/component/layout vscode格式化快捷键&#xff1a;shiftaltf table表格 <template><el-table:data"tableData"style"width: 100%"><el-table-columnprop"date"label"日期…

idea在controller或者service使用ctrl+alt+b进入方法后,如何返回到 进入前的那一层

idea在controller或者service使用ctrlaltb进入方法后&#xff0c;如何返回到进入方法的最外层 解决方案使用 ctrlalt ← /→← /→ 键盘上的左右键盘

Linux 搭建私有yum源仓库

一、环境准备 IP系统版本作用192.168.140.155CentOS 7.9.2009yum源仓库192.168.140.153CentOS 7.9.2009测试 准备两台服务器&#xff0c;一台作为yum源仓库&#xff0c;另一台作为测试使用。 二、搭建yum源服务器 &#xff08;无法连接外网的情况&#xff0c;需要去官网下载镜…

MobX入门指南:快速上手状态管理库

一、什么是MobX MobX 是一个状态管理库&#xff0c;它可以让你轻松地管理应用程序的状态&#xff0c;并且可以扩展和维护。它使用观察者模式来自动传播你的状态的变化到你的 React 组件。 二、安装及配置 安装 MobX 和 MobX-React&#xff1a;你可以使用 npm 或 yarn 安装这…

MDK stm32怎么生成bin文件

第一种 D:\Keil_v5\ARM\ac5.6\bin\fromelf.exe --bin -o ../../Output/atk_f407.bin ../../Output/atk_f407.axf 空格解析 D:\Keil_v5\ARM\ac5.6\bin\fromelf.exe一个空格--bin一个空格-o两个空格../../Output/atk_f407.bin ../../Output/atk_f407.axf &#xff08;注意后…

Python零基础从小白打怪升级中~~~~~~~多线程

线程安全和锁 一、全局解释器锁 首先需要明确的一点是GIL并不是Python的特性&#xff0c;它是在实现Python解析器(CPython)时所引入的一个概念。 GIL全称global interpreter lock&#xff0c;全局解释器锁。 每个线程在执行的时候都需要先获取GIL&#xff0c;保证同一时刻只…