Multithreaded Winsock

Logo of Vovisoft

Multithreaded Winsock

Vb6 cho ta Winsock Control để giúp một program VB6 nói chuyện với một program khác trên mạng TCP/IP.
Ta có thể dùng Winsock Control trong một program để làm Winsock Server hay Winsock Client. Sự khác biệt nầy rất nhỏ, mặc dầu ta phải lưu ý để phân biệt sự khác nhau của hai trường hợp. Giả sử ta dùng Winsock Control làm Server trong một VB6 program để chạy trên một computer và dùng Winsock Control làm Client trong một VB6 program để chạy trên một computer khác trên mạng TCP/IP. Ðể cho hai programs nói chuyện (communicate) trước hết ta cần phải connect (nối) chúng lại với nhau.
Ta cho Winsock Server Listen (lắng nghe) qua một LocalPort (một cổng có mang một con số, thí dụ như 9123). Kế đó ta cho Winsock Client Connect (móc nối) qua LocalPort đó ở địa chỉ TCP của Computer nơi ta chạy Winsock Server program. Sở dỉ ta cần phải nói rõ LocalPort số mấy là vì Server Computer có thể Listen qua nhiều LocalPorts cùng một lúc để nhiều Clients có thể Connect đến cùng một Computer TCP address. (Nếu bạn còn mới đối với TCP/IP hãy đọc bài Căn bản TCP/IP )

Class ServerWinsock và Class ClientWinsock

Trong .NET, Winsock được thay thế bằng TcpListenerTcpClient của System.Net.Sockets. Để dùng chúng ta chỉ cần Project | Add Reference.. cái System.dll và thêm câu:

Imports System.Net.Sockets ' for TcpClient and TcpServer
ở đầu phần code.

Khi instantiate một TcpListener object, ta cho nó một PortNo để nó lắng nghe qua cổng đó như sau:

Dim oListener As TcpListener ' Variable for TcpListener
' Instantiate a TcpListener on given PortNo
oListener = New TcpListener(PortNo) 
oListener.Start() ' Start the TcpListener
Về phía Client, ta gọi method Connect của TcpClient với tên của destination/server computer (hay TCP address của computer ấy) và cái cổng trên destination/server computer. Ta code như sau:

Dim Client As TcpClient ' Variable for the Client TCP socket
' Instantiate TCPClient object
Client = New TcpClient() 
' Attempt to connect to destination (server) computer on given port number
Client.Connect(DestinationComputer, TCPIPPortNo) 
Bên TcpListener sẽ dùng một Socket để Accept (nhận) cái Request (thỉnh cầu) của TcpClient:

' Accept request from the TcpClient
Dim oSocket As Socket 
oSocket = oListener.AcceptSocket 
Khi TcpListener AcceptSocket rồi thì hai bên TcpClient và TcpListener có thể gởi thông điệp qua lại cho đến khi một bên terminates (stop).
Dưới đây là hình minh họa sự móc nối và gởi thông điệp từ Client (máy SAIGON) qua Server (máy SADEC). Từ Server ta cũng có thể gởi thông điệp qua Client cùng một cách như vậy.

Một khi connection đã đứt đoạn, không dễ cho ta nối lại. Trên nguyên tắc, hai bên phải đóng socket rồi tìm cách lắng nghe/móc nối trở lại.

.NET cho ta một giải pháp đơn giản và thanh tao, đó là dùng thread, một dạng process nhẹ ký. Ở cùng một cổng, mỗi khi nhận được Request-to-connect từ một TcpClient, ta instantiate một Socket chạy trong một thread riêng để phục vụ TcpClient ấy. Khi TcpClient disconnects thì ta cũng đóng socket nầy.

Bên phía TcpClient, mỗi lần cần gởi một thông điệp ta instantiate một TcpClient mới, và sau khi gởi xong ta disconnect nó ngay.

Cách dùng thread rất đơn giản. Muốn một Sub chạy riêng trong một thread ta chỉ cần instantiate một thread với AddressOf của Sub ấy, rồi khởi động thread ấy như sau:

' create a thread to handle this Client Request
Dim oThread As Thread 
oThread = New Thread(AddressOf ProcessRequest) 
oThread.Start() ' Run Sub ProcessRequest
Để dùng Thread ta chỉ cần thêm câu:

Imports System.Threading ' for Thread
ở đầu phần code.

Trong dự án nầy, TcpListener được gói trong class ServerWinsock và TcpClient được gói trong class ClientWinsock. Chính bên trong class ServerWinsock ta dùng multithread để phục vụ nhiều TcpClient qua cùng một cổng TCPPortNo duy nhất.

Class ClientWinsock chỉ gởi thông điệp và class ServerWinsock chỉ nhận thông điệp. Khi ServerWinsock nhận một thông điệp nó sẽ Raise một Event để program chủ của nó xử lý thông điệp. Thông điệp được gởi đi lại dưới dạng một array of bytes. Do đó muốn gởi một Text String ta phải cho biết Encode của Text string lúc bấy giờ là UTF8, Unicode hay ASCII, và đổi nó ra array of bytes như sau:

Dim Buffer() As Byte ' used for outgoing message
' Convert UFT8 message to an array of bytes before sending
Buffer = System.Text.Encoding.UTF8.GetBytes(mMessage.ToCharArray) 
' Send out the buffer
Client.GetStream().Write(Buffer, 0, Buffer.Length) 
Về phía đầu ServerWinsock, khi nhận được array of bytes thì phải đổi ra Text string trở lại như sau:

' Convert the array of bytes (i.e. the buffer) to UTF8 text string
RecvMessage = System.Text.Encoding.UTF8.GetString(Buffer) 
' Raise an event to return the message to the program that owns this ServerWinsock
RaiseEvent OnMessage(RecvMessage) 
Trong thí dụ nầy ta dùng UTF8 để gởi Unicode. Nếu dữ kiện chỉ là ASCII thì có thể dùng encoding ASCII cho hiệu lực hơn vì mỗi ASCII character chỉ cần một byte:

Buffer = System.Text.Encoding.ASCII.GetBytes(mMessage.ToCharArray)   ' Chuẩn bị Buffer để gởi đi

RecvMessage = System.Text.Encoding.ASCII.GetString(Buffer)  ' Đởi lại thành ASCII text string khi nhận
Thật ra để gởi Unicode ta cũng có thể dùng encoding Unicode, tức là UTF16 LittleEndian. (Nếu bạn còn mới đối với Unicode encoding hãy đọc bài Dùng Unicode chữ Việt trong .NET).

Dưới đây là mã nguồn của hai classes ClientWinsock và ServerWinsock:

Imports System.Threading ' for threads
Imports System.Net.Sockets ' for TcpClient and TcpServer

' This module contains two classes: ClientWinsock and ServerWinsock
Public Class ClientWinsock 
  ' This object is created to connect to a TCPServer and to send a single Unicode message
   Private ClientThread As Thread ' used to run the main Sub StartClient of Client
   Private TCPIPPortNo As Integer ' TCPIP port number on destination computer
   Private DestinationComputer As String ' name or IP address of destination computer
   Private mMessage As String ' Unicode message to be sent

   Public Sub New( ByVal Destination As String, ByVal Message As String) 
     ' Split the given Message into Destination computer and TCPIP port number
      Dim pos As Integer 
     ' Locate the character ";" in the Message string
      pos = Destination.IndexOf(";") 
      If pos > 0 Then 
        ' the part before ";" is the name or IP address of destination computer
         DestinationComputer = Destination.Substring(0, pos) 
         TCPIPPortNo = CInt(Destination.Substring(pos + 1)) ' convert string to integer
      Else 
        ' character ";" does not exist, that means only TCPIP Port number is given for Localhost
         DestinationComputer = "Localhost" ' Destination computer is Localhost
         TCPIPPortNo = CInt(Destination) ' convert string to integer
      End If 
      mMessage = Message ' assign outgoing message to local string variable
     'Create a Thread object for Sub StartClient
      ClientThread = New Thread(AddressOf StartClient) 
     'Starting the thread invokes the ThreadStart delegate
     ' i.e. run Sub StartClient in its own thread
      ClientThread.Start() 
   End Sub 

   Protected Sub StartClient() 
     ' This is the main code in ClientWinsock. It's run in its own thread
      Dim Client As TcpClient ' Variable for the Client TCP socket
      Dim Buffer() As Byte ' used for outgoing message
      Try 
        ' Instantiate TCPClient object
         Client = New TcpClient() 
        ' Attempt to connect to destination (server) computer on given port number
         Client.Connect(DestinationComputer, TCPIPPortNo) 
        ' Convert UFT8 message to an array of bytes before sending
         Buffer = System.Text.Encoding.UTF8.GetBytes(mMessage.ToCharArray) 
        ' Send out the buffer
         Client.GetStream().Write(Buffer, 0, Buffer.Length) 
         Client.Close() ' Close the TcpClient
      Catch e As Exception 
        ' Write to Console the message that cannot be sent
         Console.WriteLine("Can 't send:" & mMessage)
         Finally 
         ClientThread.Abort() ' Abort thread
      End Try 
   End Sub 
End Class 

Public Class ServerWinsock 
  ' This object is created to serve many TCPClients and to receive Unicode messages
  ' A thread is created to serve each TCPClient
   Const MaxThread As Integer = 500 ' Maximum number of threads that ServerWinsock can handle
   Private oListener As TcpListener ' Variable for TcpListener
   Private bStopListener As Boolean ' Flag indicating that user wants to dispose this ServerWinsock
   Private ActiveThreads As Integer ' Number of active threads, i.e. threads that are serving TCPClients
  ' Event that returns the incoming message
   Public Event OnMessage( ByVal IncomingMessage As String) 

   Public Sub New( ByVal PortNo As Integer) 
     ' Instantiate a TcpListener on given PortNo
      oListener = New TcpListener(PortNo) 
      oListener.Start() ' Start the TcpListener
     ' Create a thread for Sub AcceptConnection
      Dim ServerThread As Thread 
      ServerThread = New Thread(AddressOf AcceptConnection) 
      ServerThread.Start() ' Run Sub AcceptConnection like a light weight child process
   End Sub 

   Protected Sub ProcessRequest() 
      Dim Buffer(5000) As Byte ' used to receive incoming message from TcpClient
      Dim bytes As Integer ' Actual number of bytes read
      Dim RecvMessage As String ' UTF8 text string of the buffer (array of bytes)
     ' Use oThread to reference the thread of this Sub
      Dim oThread As Thread 
      oThread = Thread.CurrentThread() 
     ' Accept request from the TcpClient
      Dim oSocket As Socket 
      oSocket = oListener.AcceptSocket 
     ' Keep looping until user wants to stop
      While Not bStopListener 
         If oSocket.Available > 0 Then ' A message has arrived from TcpClient
           ' read the incoming message into a buffer
            bytes = oSocket.Receive(Buffer, Buffer.Length, 0) 
            SyncLock oThread ' Lock oThread
           ' Convert the array of bytes (i.e. the buffer) to UTF8 text string
            RecvMessage = System.Text.Encoding.UTF8.GetString(Buffer) 
           ' Raise an event to return the message to the program that owns this ServerWinsock
            RaiseEvent OnMessage(RecvMessage) 
            End SyncLock ' unlock oThread
            Exit While 
         End If 
        ' get out of while loop if TcpClient has disconnected
         If Not oSocket.Connected Then Exit While 
      End While 
      oSocket.Close() ' Close the TcpServer socket
      SyncLock oThread ' Lock oThread
      ActiveThreads -= 1 ' Decrement number of Active Threads
      End SyncLock ' unlock oThread
   End Sub 

   Private Sub AcceptConnection() 
     'This is the main Sub of ServerWinsock
     ' Keep looping until user wants to stop
      Do While Not bStopListener 
         Thread.Sleep(100) ' Sleep 100 msec.
         If oListener.Pending() Then ' received a request for connection from a TCPClient
            If ActiveThreads <= MaxThread Then 
              ' create a thread to handle this Client Request
               Dim oThread As Thread 
               oThread = New Thread(AddressOf ProcessRequest) 
               oThread.Start() ' Run Sub ProcessRequest
               SyncLock oThread ' Lock oThread so that ActiveThreads value is not changed while we're adding 1 to it
               ActiveThreads += 1 ' Increment number of active threads
               End SyncLock ' Release the lock on oThread
            End If 
         End If 
      Loop 
   End Sub 

   Protected Overloads Sub Dispose( ByVal disposing As Boolean) 
       bStopListener = True ' Indicate user wants to dispose this ServerWinsock object
       oListener.Stop() ' Stop the TcpListener
    End Sub 

End Class 

Chương trình dùng để thử ServerWinsock và ClientWinsock

Để thử ServerWinsock và ClientWinsock, một TestBed Form được thiết kế để lăn-xê một form cho mỗi computer mà ta muốn đặt một ServerWinsock trên ấy. Ta phải nói rõ computer tên gì (hay cho TCP address của nó cũng được) và TCPPortNo, tức là con số của cổng mà nó lắng nghe.

Khi khởi động, chương trình thử cho hiển thị hai TCPIPTestForms để bạn có thể thử ngay bằng cách click nút Send
Bây giờ nếu bạn click nút Add Station trên TestBed form, con số TCP Port sẽ được thêm vào listbox Active Servers và một TCPIPTestForm mới sẽ được hiển thị. Click nút Send trên form mới nầy, bạn sẽ thấy message được gởi đến hai TCPIPTestForms có sẵn như sau:

Bạn có thể tải về mã nguồn của chương trình nầy kể cả hai classes ClientWinsock và ServerWinsock.
Trong Zip file có chứa 2 projects: TCPQueue và clsWinsock. Nếu có gặp trở ngại về việc reference thì load clsWinsock và compile trước để dùng clsWinsock.dll trong bin folder làm reference cho project TCPQueue. Sau khi load TCPQueue, bạn hãy remove clsWinsock từ References của nó rồi dùng Menu Command Project |Add Reference để refernce library clsWinsock.dll vừa mới compile.

Nếu bao giờ IDE than phiền về Licence bạn chỉ cần delete file licences.licx trong Project Folder.
  Học Microsoft .NET