zoukankan      html  css  js  c++  java
  • 非常完整的局域网聊天工具[转]

    LanTalk_screenshoot.jpg

    Contents

    1. Introduction
    2. The demo application
    3. An introduction into sockets, connection-oriented protocols (TCP)
    4. LanTalk.Core – core functionality and shared objects
    5. The server application
    6. The client application
    7. Logging - NLog
    8. Conclusions

    1. Introduction

    Sockets, sockets, sockets! Or was it developers, developers, developers? Sockets are used by nearly every program out there, so they deserve their own place on the stand. The birth of this article was caused by the need of a simple chat application that can be run in service mode (like, run once forget forever mode). There are a few good articles here on how to implement a service, using sockets, so I did my research. The resulted application will be dissected in the following pages.

    So, grab yourself a coffee, it's going to be a long and, hopefully, interesting read.

    2. The demo application

    This is a typical client-server application. Provided is a GUI server application, a server implemented as a service and a client application.

    After the server starts, it accepts TCP connections on port 65534 (or by a port set by the user). Users connected to the server must authenticate, before any message exchange takes place. The possibility of public and private messaging is implemented, and a basic logging mechanism.

    One may use the GUI server application or the service to accept incoming requests and to serve the clients. However, you should know that in this version, only the GUI server app can be used to create users.

    2.1. Setting up the demo application

    1. Edit the Settings.xml file in the LanTalk.Server\Data directory.
    2. Run the LanTalk.ServerGui application and create some users.
    3. Install the service by running “LanTalk.Service install”.
    4. Start the server using LanTalk.ServerGui, or start the service by running “net start LanTalk”.
    5. Fire up the LanTalk.Client application.
    6. Set the connection settings in the Application -> Application Settings menu.
    7. Log in to the server, and start talking.

    3. An introduction into sockets, connection-oriented protocols (TCP)

    An introduction into IP addresses and the TCP/IP model can be found here[^]. I will not delve into this.

    The communication between the server and the client is implemented using the TCP protocol. TCP is a connection-oriented protocol, meaning that it provides reliable, in-order delivery of a stream of bytes. Using TCP applications, we can exchange data using stream sockets. A socket is a communication endpoint. For an endpoint to be uniquely identified, it needs to contain the following data: the protocol used to communicate, an IP address, and a port number.

    In the .NET framework, a socket is represented by the Socket class in the System.Net.Sockets namespace. TheSocket class defines the following members to uniquely describe an endpoint:

    For the protocol in use:

    public AddressFamily AddressFamily 
    { get; }
    Specifies the addressing scheme that an instance of the Socket class can use. The two most common AddressFamily values used areInterNetwork for IPV4 addresses, and InterNetworkV6 for IPV6 addresses.
    public SocketType SocketType
    { get; }
    Gets the type of the socket. The SocketType indicates the type of communication that will take place over the socket, the two most common types being Stream (for connection-oriented sockets) andDgram (for connectionless sockets).
    public ProtocolType ProtocolType
    { get; }
    Specifies the protocol type of the Socket.

    For the local/remote endpoint:

    public EndPoint LocalEndPoint/RemoteEndPoint
    { get; }
    Specifies the local/remote endpoint.

    3.1. The EndPoint / IPEndPoint class

    It’s an abstract class that identifies a network address. The IPEndPoint class is derived from the EndPoint class, and represents a network endpoint (a combination of an IP address and a port number).

    public IPAddress Address 
    { get; set; }
    Gets or sets the IP address of the endpoint.
    public int Port
    { get; set; }
    Gets or sets the port number of the endpoint.

    3.2. Steps to create connection-oriented sockets on the server side

    Before the server can accept any incoming connection, you must perform four steps on a server socket:

      1. Create the socket. When a socket is created, it is given an address family and a protocol type, but it has no assigned address and port.
    socket = new Socket(AddressFamily.InterNetwork, 
             SocketType.Stream, ProtocolType.Tcp);
      1. Bind that socket to a local address and port. Bind assigns a socket an address and a port.
    endPoint = new IPEndPoint(IPAddress.Parse(“127.0.0.1”), 65534);
    socket.Bind(endPoint);
      1. Prepare the bound socket to accept incoming connections. The Listen method sets up internal queues for asocket. Whenever a client attempts to connect, the connection request is placed in the queue. Pending connections from the queue are pulled with the Accept method that returns a new Socket instance.
    socket.Listen(32);    // a 32 maximum pending connections
      1. Accept incoming connections on the socket.
    socket.BeginAccept(new AsyncCallback(HandleIncommingClient), null);

    3.3. Steps to create connection-oriented sockets on the client side

    To create a socket on the client side, you need to create a socket and wire up that socket to the remote address and port. That simple! To connect to the remote server, you have to use the Socket.Connect method:

    socket.Connect(serverEndPoint);

    3.4. Blocking on sockets

    The Socket class methods use blocking, by default, this is why when execution reaches a network call that blocks, the application waits until the operation completes (or an exception is thrown). To avoid blocking on sockets, we may choose to use non-blocking sockets, multiplexed sockets, or asynchronous sockets. There is a fourth way to use a socket, by handling connections with IOCP (IO completion ports).

    3.4.1. Non-blocking sockets

    Setting the Socket.Blocking property to false can set your socket to function as non-blocking. When a socketis in non-blocking mode, it will not wait for a requested operation to complete; rather, it will check the operation, and if it can’t be completed, the method will fail and program execution will continue.

    Disadvantages of non-blocking sockets: although this allows the program to do other things while the network operations finish, it requires that the program repeatedly poll to find out when each request has finished.

    3.4.2. Multiplexed sockets

    This technique is used to watch multiple Socket instances for the ones that are ready to be read or written to. For this, the Socket.Select(…) static method is used. The format of the Select method is:

    public static void Select (
        IList checkRead,
        IList checkWrite,
        IList checkError,
        int microSeconds
    );

    Select returns when at least one of the sockets of interest (the sockets in the checkReadcheckWrite, andcheckError lists) meets its specified criteria, or the microSeconds parameter is exceeded, whichever comes first. Setting microSeconds to -1 specifies an infinite time-out. This is a combination of non-blocking sockets. One way to use Select is to create a timer that fires frequently, and within the timer's event handler, call Select with a timeout value of zero so that the call is non-blocking.

    For a detailed example, look in MSDN.

    Disadvantages of multiplexed sockets: this is an inefficient method, because the time needed to serve thesockets depends on how frequently the timer is fired (the microSeconds parameter). Also, the servicing of thesockets prevents the code from reentering the Select method, which may cause starvation of threads.

    3.4.3. Asynchronous sockets

    Asynchronous sockets use callback methods to handle incoming data and connections. By convention, an asynchronous socket method begins with the word “Begin”, like BeginReceiveBeginAccept, etc. This method allows you to use a separate method when a socket operation is available (the delegate will be called). The asynchronous calls use the AsyncCallback class to call completion methods when a socket operation is finished.

    The Socket class has the following asynchronous methods:

    public IAsyncResult BeginAccept( … )
    public IAsyncResult BeginConnect( … )
    public IAsyncResult BeginReceive( … )
    public IAsyncResult BeginSend( … )

    Each of the Begin methods starts the network function, and registers the AsyncCallback method. Each of the Begin methods has to have an End method that completes the function when the AsyncCallback method is called.

    The ways of multi-threading in .NET are: starting your own threads with ThreadStart delegates, and using theThreadPool class either directly (using ThreadPool.QueueUserWorkItem), or indirectly by using asynchronous methods (such as Stream.BeginRead, or calling BeginInvoke on any delegate). Asynchronous I/O uses the application's thread pool, so when there is data to be processed, a thread from the thread pool performs the callback function. This way, a thread is used for a single I/O operation, and after that, it is returned to the pool.

    3.4.5. IOCP requests

    IOCP represents a solution to the one-thread-per-socket problem. Completion ports are used in high performance servers, with a high number of concurrent connections. In this article, we are focusing on asynchronous sockets.

    3.5. Closing down the socket

    For closing the socket, the client application is advised to use the Shutdown method. This allows all the unsent data to be sent and the un-received data to be received before the connection is closed:

    if (socket.Connected) socket.Shutdown(SocketShutdown.Both);
    socket.Close();

    The SocketShutdown enumeration defines what operation on the socket will be denied (SendReceive, or Both).

    3.6. Exception handling and important error codes

    If a socket method encounters an error, a SocketException is thrown. The ErrorCode member contains the error code associated with the exception thrown. This is the same error code that is received by calling theWSAGetLastError of the WinSock library.

    Error Number Winsock Value SocketError Value Meaning
    10004 WSAEINTR Interrupted The system call was interrupted. This can happen when a socket call is in progress and the socket is closed.
    10048 WSAEADDRINUSE AddressAlreadyInUse The address you are attempting to bind to or listen on is already in use.
    10053 WSACONNABORTED ConnectionAborted The connection was aborted locally.
    10054 WSAECONNRESET ConnectionReset The connection was reset by the other end.
    10061 WSAECONNREFUSED ConnectionRefused The connection was refused by the remote host. This can happen if the remote host is not running, or if it is unable to accept a connection because it is busy and the listen queue is full.

    3.7. A few words about building scalable server applications

    When thinking about building scalable server applications, we have to think about thread pools. Thread pools allow a server application to queue and perform work in an efficient way. Without thread pools, the programmer has two options:

      1. Use a single thread and perform all the work using that thread. This means linear execution (one has to wait for an operation to terminate before proceeding on).

    or

    1. Spawn a new thread every time a new job needs processing. Having multiple threads performing work at the same time does not necessarily mean that the application is becoming faster or is getting more work done. If multiple threads try to access the same resource, you may have a block scenario where threads are waiting for a resource to become available.

    In the .NET framework, the “System.Threading” namespace has a ThreadPool class. Unfortunately, it is a static class, and therefore our server can only have a single thread pool. This isn't the only issue. The ThreadPool class does not allow us to set the concurrency level of the thread pool.

    4. LanTalk.Core - core functionality and shared objects

    The LanTalk.Core assembly contains the common classes used by both the server and the client application.

    4.1 The ChatSocket class

    The ChatSocket class is a wrapper around a Socket, extending with other data:

    ChatSocket_class.jpg

    The ChatSocket class has three public methods:

      • Send - Using the underlying socket, writes out the encapsulated data to the network. This is done in the following way: the first four bytes are sent - a ChatCommand enumeration type that notifies the receiver of the command the sender wants to carry out. Then, the command target IP is sent. The last step is to send the metadata (the actual data - more on this later).

    ChatSocket.Send.jpg

    Raises the Sent event if the sending operation was successful. On exception, closes the socket and raises theDisconnected event.

    • Receive - Reads the data using the wrapped socket asynchronously. Basically, the same steps are performed as in the Send. If an unknown command was sent, the user of the ChatSocket object disconnects thesocket. Raises the Received event, only if a known command was received. On exception, closes the socketand raises the Disconnected event.
    • Close - Flushes the data and closes the socket. Doesn't raise any event.

    4.2. A word about metadata and the use of reflection

    The metatype data is the string representation of the object used in the communication between the client and the server, something like: “LanTalk.Core.ResponsePacket, LanTalk.Core, Version=1.8.3.2, Culture=neutral, PublicKeyToken=null”, where the meta buffer contains the actual object.

    Each metatype received should implement the IBasePacket interface:

    interface IBasePacket
    {
        /// <summary>
        /// Initialize the class members using the byte array passed in as parameter.
        /// </summary>
        void Initialize(byte[] metadata);
        
        /// <summary>
        /// Return metadata (a byte array) constructed from the members of the class.
        /// </summary>
        byte[] GetBytes();
    }   // IBasePacket

    The BasePacket class defines the Initialize method to initialize the data members of an IBasePacket derived object using the Type.GetField method. This is used when a byte array is received from the network (the meta buffer), and in the knowledge of the metadata type, we have to initialize the created object.

    public void Initialize(byte[] data)
    {
        Type type = this.GetType();
        FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | 
                                            BindingFlags.Instance);
    
        int ptr = 0;
        int size = 0;
    
        try
        {
            for (int i = 0; i < fields.Length; i++)
            {
                FieldInfo fi = fields[i];
    
                // get size of the next member from the byte array
                size = BitConverter.ToInt32(data, ptr);
                ptr += 4;
    
                if (fi.FieldType == typeof(System.String))
                {
                    fi.SetValue(this, Encoding.Unicode.GetString(data, ptr, size));
                    ptr += size;
                }
            }
        }
        catch (Exception exc)
        {
            Trace.Write(exc.Message + " - " + exc.StackTrace);
        }
    }

    The ChatSocket class uses the Initialize method in the following way:

    Type typeToCreate = Type.GetType(metatype);
    IBasePacket packet = (IBasePacket)Activator.CreateInstance(typeToCreate);
    packet.Initialize(metaBuffer);

    The GetBytes method's role is to return a byte array representing the internal state of the calling object. It is used to create the byte array that the socket sends out to the network.

    public byte[] GetBytes()
    {
        byte[] data = new byte[4];
        int ptr = 0;
        int size = 0;
    
        Type type = this.GetType();
        FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | 
                                            BindingFlags.Instance);
    
        try
        {
            for (int i = 0; i < fields.Length; i++)
            {
                FieldInfo fi = fields[i];
    
                if (fi.FieldType == typeof(System.String))
                {
                    // get the string value of the field
                    string strField = (string)fi.GetValue(this);
    
                    // get size of from string, copy size into array
                    size = Encoding.Unicode.GetByteCount(strField);
                    BasePacket.ResizeDataBuffer(ref data, ptr, size);
                    Buffer.BlockCopy(BitConverter.GetBytes(size), 0, data, ptr, 4);
                    ptr += 4;   // GetBytes returns the size as an array of 4 bytes
    
                    // copy string value into array
                    BasePacket.ResizeDataBuffer(ref data, ptr, size);
                    Buffer.BlockCopy(Encoding.Unicode.GetBytes(strField), 0, data, ptr, size);
                    ptr += size;
                }
            }
        }
        catch (Exception exc)
        {
            Trace.Write(exc.Message + " - " + exc.StackTrace);
        }
    
        byte[] retData = new byte[ptr];
        Array.Copy(data, retData, ptr);
    
        return retData;
    }

    This method is used when sending data using the ChatSocket class, as follows:

    byte[] metaBuffer = null;
    Type metaType = metadata.GetType();
    MethodInfo mi = metaType.GetMethod("GetBytes");
    metaBuffer = (byte[])mi.Invoke(metadata, null);
    // get the byte representation of the metadata

    The classes derived from the IBasePacket interface are used to encapsulate information into a known format and then communicate the object to the other party. The benefits of using Reflection is that defining new packet types becomes trivial.

    5. The server application

    The server is implemented as a threaded server, meaning that each new connection spawns a new thread. This implementation does not scale well with a large number of clients, but for my purposes, I only need a maximum of 32 connections. The main drawback is that each running thread has its own stack, which by default is one megabyte in size. This also means that on 32 bit machines with the default of one megabyte stack size, you can create only roughly 2000 threads before you run out of the 2GB user address space per process. This can be avoided by using IO completion ports.

    The LanTalk.Server.dll assembly contains the server implementation, and is used by the GUI and the service application. When starting the server, the following operations are performed:

      1. The settings are read from the Data\Settings.xml file
      2. A new socket is created using the IP address and port specified in the settings file (in case of error, thesocket is created on 127.0.0.1:65534)
      3. Associate the socket with the created endpoint (the [IP, port] combination) - this is called binding the socket
      4. Place the socket in listening state
      5. If all goes well, the server fires the Started event and begins accepting the connections asynchronously:
    socket.BeginAccept(new AsyncCallback(HandleIncommingClient), null);
    
    private void HandleIncommingClient(IAsyncResult aResult)
    {
        ....
        Socket clientSocket = socket.EndAccept(aResult);
        // process the connected socket
        ....
        // continue accepting incomming connections
        socket.BeginAccept(new AsyncCallback(HandleIncommingClient), null);
        ...
    }

    The ClientManager class

    The ChatServer object maintains the list of connected clients. For this, when a connection is made, it creates aClientManager object. The ClientManager object holds the connected socket and some user related information. Its responsibility is to send and receive data, and when that happens, to signals it with events. Also, theClientManager updates user related information such as last connection time and last IP address of the connection.

    6. The client application

    The Client class is implemented as a singleton, only one instance of the client can be created. The Client class has three methods exposed to the outside (ConnectDisconnectSendCommand); the rest of the functionality exposed is implemented through the use of events.

    Client_class.jpg

    This is a simple class with four core functionalities:

    1. Connect to the server - the Connect method - fires the Connected event on successful connection, or theConnectFailed event in case of error.
    2. Send messages to the server - the SendCommand method.
    3. Receive messages from the server - responds to the ChatSocket's Received event - fires theCommandReceived event.
    4. Disconnect from the server - the Disconnect method - closes the underlying socket and fires theDisconnected event.

    The Client object uses the TCP keep-alive feature to prevent disconnecting the clients from the server in case of inactivity:

    private void OnConnect(IAsyncResult ar)
    {
        // send keep alive after 10 minute of inactivity
        SockUtils.SetKeepAlive(socket, 600 * 1000, 60 * 1000);
    }

    The Settings.xml file is used for persisting application settings, and is saved in the currently logged on user's profile folder. The exact location is: “%Documents and settings%\%user%\Application Data\Netis\LanTalk Client”.

    Archiving messages is also provided on a per Windows user/per LanTalk user basis. For example, for me:

    • The directory “C:\Documents and Settings\zbalazs\Application Data\Netis\LanTalk Client\” will hold theSettings.xml file, and I will have a directory created with the username which I use to authenticate in the LanTalk application (“zoli”).
    • The “C:\Documents and Settings\zbalazs\Application Data\Netis\LanTalk Client\zoli\” directory will hold the message archive for me.

    7. Logging - NLog

    For logging, the application uses the NLog library. NLog provides a simple logging interface, and is configured by theapplication.exe.nlog file. If this file is missing, logging will not occur.

    8. Conclusions

    I recommend downloading the samples provided and playing with it a few times. After that, check out the sources. I hope that the article follows a logical line describing briefly the highlights of the projects.

    References

    1. Transmission control protocol[^]
    2. Which I/O strategy should I use?[^]
    3. Get Closer to the Wire with High-Performance Sockets in .NET[^]
    4. TCP/IP Chat Application Using C#[^]
    5. TCP Keep-Alive overview [^]
    6. How expensive is TCP's Keep-Alive feature?[^]
    7. TCP/IP and NBT configuration parameters for Windows XP [^]
    8. DockManager control [^]
    9. TaskbarNotifier[^]
    10. Self installing .NET service using the Win32 API[^]
    11. NLog[^]

    License

    This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

    About the Author

    Zoltan Balazs

    Software Developer

    Romania Romania

    Member
    I've did some programming for Macintosh a few years back and working with Microsoft technologies since then.

    http://www.codeproject.com/KB/IP/LanTalkChat.aspx

  • 相关阅读:
    [上海线下活动]IT俱乐部新春首期活动: 高级Windows调试
    清除www.fa899.com
    [新功能]总是只列出标题
    [功能改进]更多的列表数定制
    新增Skin使用排行榜
    华硕P5GDCV Deluxe主板更换RAID 1中的故障硬盘步骤
    [WebPart发布]网站链接WebPart
    [通知]19:3020:30进行服务器维护
    [小改进]个人Blog首页显示随笔摘要
    新增两款Skin(clover与Valentine)
  • 原文地址:https://www.cnblogs.com/saptechnique/p/2298082.html
Copyright © 2011-2022 走看看