65.9K
CodeProject 正在变化。 阅读更多。
Home

使用异步 TCP 套接字的聊天应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (62投票s)

2006年12月28日

3分钟阅读

viewsIcon

491011

downloadIcon

33114

本文将讨论一个使用 C# 中的异步 TCP Socket 的聊天应用程序。

引言

在本文中,我将讨论一个使用 C# 中的异步 TCP Socket 的聊天应用程序。 在本文的下一部分中,我将介绍一个基于异步 UDP Socket 的聊天应用程序。

TCP 异步 Socket

TCP 异步 Socket 在标准 Socket 函数后面附加了BeginEnd,例如 BeginConnectBeginAcceptBeginSendBeginReceive。 让我们来看其中一个。

IAsyncResult BeginAccept(AsyncCallback callback, object state);

当函数完成时,会调用 AsyncCallback 函数。 就像事件可以触发委托一样,.NET 也提供了一种方法让方法触发委托。.NET AsyncCallback 类允许方法启动异步函数,并提供一个委托方法,以便在异步函数完成时调用。

state 对象用于在 BeginAccept 和相应的 AsyncCallback 函数之间传递信息。

以下展示了它的实现:

Socket sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream,
                         ProtocolType.Tcp);
IPEndPoint iep = new IPEndPoint(IPAddress.Any, 9050);
sock.Bind (iep);
sock.Listen (5);
sock.BeginAccept (new AsyncCallback(CallAccept), sock);

private void CallAccept(IAsyncResult iar)
{
    Socket server = (Socket)iar.AsyncState;
    Socket client = server.EndAccept(iar);
}

原始的 sock 对象在 CallAccept 中通过使用 IAsyncResultAsyncState 属性来检索。 一旦我们有了原始的 Socket(操作在其上完成),我们就可以获取客户端 Socket 并对其执行操作。

所有其他的异步 Socket 函数都像这样工作,您应该在 MSDN 上查找更多详细信息。网上有一些不错的文章解释了这些细节,也请您搜索一下。

入门

为了在客户端和服务器之间交换消息,它们都使用以下简单的命令:

//The commands for interaction between the server and the client
enum Command
{
    //Log into the server
    Login,      
    //Logout of the server
    Logout,
    //Send a text message to all the chat clients     
    Message,    
    //Get a list of users in the chat room from the server
    List
}

用于在客户端和服务器之间交换数据的数据结构如下所示。 Socket 以字节数组的形式发送和接收数据;重载的构造函数和 ToByte 成员函数执行此转换。

//The data structure by which the server and the client interact with 
//each other
class Data
{
    //Default constructor
    public Data()
    {
        this.cmdCommand = Command.Null;
        this.strMessage = null;
        this.strName = null;
    }

    //Converts the bytes into an object of type Data
    public Data(byte[] data)
    {
        //The first four bytes are for the Command
        this.cmdCommand = (Command)BitConverter.ToInt32(data, 0);

        //The next four store the length of the name
        int nameLen = BitConverter.ToInt32(data, 4);

        //The next four store the length of the message
        int msgLen = BitConverter.ToInt32(data, 8);

        //Makes sure that strName has been passed in the array of bytes
        if (nameLen > 0)
            this.strName = Encoding.UTF8.GetString(data, 12, nameLen);
        else
            this.strName = null;

        //This checks for a null message field
        if (msgLen > 0)
            this.strMessage = Encoding.UTF8.GetString(data, 
                              12 + nameLen, msgLen);
        else
            this.strMessage = null;
    }

    //Converts the Data structure into an array of bytes
    public byte[] ToByte()
    {
        List<byte> result = new List<byte>();

        //First four are for the Command
        result.AddRange(BitConverter.GetBytes((int)cmdCommand));

        //Add the length of the name
        if (strName != null)
            result.AddRange(BitConverter.GetBytes(strName.Length));
        else
            result.AddRange(BitConverter.GetBytes(0));

        //Length of the message
        if (strMessage != null)
            result.AddRange(BitConverter.GetBytes(strMessage.Length));
        else
            result.AddRange(BitConverter.GetBytes(0));

        //Add the name
        if (strName != null)
            result.AddRange(Encoding.UTF8.GetBytes(strName));

        //And, lastly we add the message text to our array of bytes
        if (strMessage != null)
            result.AddRange(Encoding.UTF8.GetBytes(strMessage));

        return result.ToArray();
    }

    //Name by which the client logs into the room
    public string strName;
    //Message text
    public string strMessage;
    //Command type (login, logout, send message, etc)
    public Command cmdCommand;
}

TCP 服务器

服务器应用程序监听特定的端口并等待客户端。 客户端连接到服务器并加入聊天室。 然后客户端向服务器发送消息,服务器然后将此消息发送给聊天室中的所有用户。

服务器应用程序具有以下数据成员:

//The ClientInfo structure holds 
//the required information about every
//client connected to the server
struct ClientInfo
{
    //Socket of the client
    public Socket socket;
    //Name by which the user logged into the chat room
    public string strName;
}

//The collection of all clients logged 
//into the room (an array of type ClientInfo)
ArrayList clientList;

//The main socket on which the server listens to the clients
Socket serverSocket;

byte[] byteData = new byte[1024];

以下是它如何开始监听客户端并接受传入的请求:

private void Form1_Load(object sender, EventArgs e)
{
    try
    {
        //We are using TCP sockets
        serverSocket = new Socket(AddressFamily.InterNetwork, 
                                  SocketType.Stream, 
                                  ProtocolType.Tcp);
        
        //Assign the any IP of the machine and listen on port number 1000
        IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Any, 1000);

        //Bind and listen on the given address
        serverSocket.Bind(ipEndPoint);
        serverSocket.Listen(4);

        //Accept the incoming clients
        serverSocket.BeginAccept(new AsyncCallback(OnAccept), null);
    }
    catch (Exception ex)
    { 
        MessageBox.Show(ex.Message, "SGSserverTCP", 
            MessageBoxButtons.OK, MessageBoxIcon.Error); 
    }
}

使用 IPAddress.Any,我们指定服务器应接受来自任何接口上的客户端请求。 要使用任何特定的接口,我们可以使用 IPAddress.Parse (“192.168.1.1”) 而不是 IPAddress.Any。 然后,Bind 函数将 serverSocket 绑定到此 IP 地址。 Listen 函数使服务器等待客户端,传递给 Listen 的值指定操作系统将排队多少个传入的客户端请求。 如果有挂起的客户端请求,并且又有新的请求进入,则该请求将被拒绝。

以下是对应的 OnAccept 函数:

private void OnAccept(IAsyncResult ar)
{
    try
    {
        Socket clientSocket = serverSocket.EndAccept(ar);

        //Start listening for more clients
        serverSocket.BeginAccept(new AsyncCallback(OnAccept), null);

        //Once the client connects then start 
        //receiving the commands from her
        clientSocket.BeginReceive(byteData, 0, 
            byteData.Length, SocketFlags.None, 
            new AsyncCallback(OnReceive), clientSocket);
    }
    catch (Exception ex)
    { 
        MessageBox.Show(ex.Message, "SGSserverTCP", 
            MessageBoxButtons.OK, MessageBoxIcon.Error); 
    }
}

EndAccept 返回连接客户端的 clientSocket 对象。 然后,我们再次通过调用 serverSocket.BeginAccept 开始监听更多传入的客户端请求;这是必不可少的,因为如果没有它,服务器将无法处理任何其他传入的客户端请求。

通过 BeginReceive,我们开始接收客户端将要发送的数据。 请注意,我们将 clientSocket 作为 BeginReceive 的最后一个参数传递;AsyncCallback OnReceive 通过 IAsyncResultAsyncState 属性获取此对象,然后处理客户端请求(登录、注销以及向用户发送消息)。 请参阅附加的代码以了解 OnReceive 的实现。

TCP 客户端

客户端使用的一些数据成员是:

//The main client socket
public Socket clientSocket;
//Name by which the user logs into the room
public string strName;

private byte[] byteData = new byte[1024];

客户端首先连接到服务器。

try
{
    //We are using TCP sockets
    clientSocket = new Socket(AddressFamily.InterNetwork,
                   SocketType.Stream, ProtocolType.Tcp);

    IPAddress ipAddress = IPAddress.Parse(txtServerIP.Text);
    //Server is listening on port 1000
    IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, 1000);

    //Connect to the server
    clientSocket.BeginConnect(ipEndPoint, 
        new AsyncCallback(OnConnect), null);
}
catch (Exception ex)
{ 
    MessageBox.Show(ex.Message, "SGSclient", 
                    MessageBoxButtons.OK, 
                    MessageBoxIcon.Error); 
}

连接后,将登录消息发送到服务器。 之后,我们发送一个列表消息以获取聊天室中客户端的名称。

//Broadcast the message typed by the user to everyone
private void btnSend_Click(object sender, EventArgs e)
{
    try
    {
        //Fill the info for the message to be send
        Data msgToSend = new Data();
        
        msgToSend.strName = strName;
        msgToSend.strMessage = txtMessage.Text;
        msgToSend.cmdCommand = Command.Message;

        byteData = msgToSend.ToByte();

        //Send it to the server
        clientSocket.BeginSend (byteData, 0, byteData.Length, 
            SocketFlags.None, 
            new AsyncCallback(OnSend), null);

        txtMessage.Text = null;
    }
    catch (Exception)
    {
        MessageBox.Show("Unable to send message to the server.", 
                        "SGSclientTCP: " + strName, 
                        MessageBoxButtons.OK, 
                        MessageBoxIcon.Error);
    }  
}

用户输入的消息作为命令消息发送到服务器,然后服务器将其发送给聊天室中的所有其他用户。

收到消息后,客户端会相应地处理它(取决于它是登录、注销、命令还是列表消息)。 此代码非常简单,请参阅附加的项目。

© . All rights reserved.