microsoft-visualbasic-runtime/ApplicationServices/Tools/Network/Tcp/Persistent/MessagePushServices/MessagePushServer.vb

413 lines
18 KiB
VB.net
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#Region "Microsoft.VisualBasic::4503a5e2e9f2543ee1661b0f2816f869, Microsoft.VisualBasic.Core\ApplicationServices\Tools\Network\Tcp\Persistent\MessagePushServices\MessagePushServer.vb"
' Author:
'
' asuka (amethyst.asuka@gcmodeller.org)
' xie (genetics@smrucc.org)
' xieguigang (xie.guigang@live.com)
'
' Copyright (c) 2018 GPL3 Licensed
'
'
' GNU GENERAL PUBLIC LICENSE (GPL3)
'
'
' This program is free software: you can redistribute it and/or modify
' it under the terms of the GNU General Public License as published by
' the Free Software Foundation, either version 3 of the License, or
' (at your option) any later version.
'
' This program is distributed in the hope that it will be useful,
' but WITHOUT ANY WARRANTY; without even the implied warranty of
' MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
' GNU General Public License for more details.
'
' You should have received a copy of the GNU General Public License
' along with this program. If not, see <http://www.gnu.org/licenses/>.
' /********************************************************************************/
' Summaries:
' Class MessagePushServer
'
' Properties: LocalPort, ProtocolHandler, Responsehandler, SSLMode, UidMappings
' UidMappingsBack
'
' Constructor: (+1 Overloads) Sub New
'
' Function: __broadcastMessage, __getMyIPAddress, __isGetSocketPortal, __isUserOnlineQuery, __Logon
' __nonUidMappings, __requestHandlerInterface, (+2 Overloads) __sendMessage, __usrInvokeSend, GetEnumerator
' IEnumerable_GetEnumerator, Run
'
' Sub: __socketCleanup, AcceptClient, DisconnectUser, Dispose, Install
' RemoveFreeConnections, Run, SendMessage
'
'
' /********************************************************************************/
#End Region
Imports System.Reflection
Imports System.Threading
Imports Microsoft.VisualBasic.ComponentModel
Imports Microsoft.VisualBasic.Linq.Extensions
Imports Microsoft.VisualBasic.Net.Abstract
Imports Microsoft.VisualBasic.Net.Http
Imports Microsoft.VisualBasic.Net.Persistent.Application.Protocols
Imports Microsoft.VisualBasic.Net.Persistent.Socket
Imports Microsoft.VisualBasic.Net.Protocols
Imports Microsoft.VisualBasic.Net.Protocols.Reflection
Namespace Net.Persistent.Application
''' <summary>
''' 长连接模式的消息推送服务器
''' </summary>
<Protocol(GetType(ServicesProtocol.Protocols))>
Public Class MessagePushServer : Inherits ServicesSocket
Implements IEnumerable(Of KeyValuePair(Of Long, WorkSocket))
Implements ITaskDriver, IDataRequestHandler
Public ReadOnly Property ProtocolHandler As ProtocolHandler
Dim _socketList As New Dictionary(Of Long, WorkSocket)
''' <summary>
''' 客户端对这个服务器的端口号是自动配置的,只需要向客户端返回<see cref="_LocalPort"/>端口就可以了
''' </summary>
Dim _workSocket As TcpSynchronizationServicesSocket
Dim _offlineMessageSendHandler As OffLineMessageSendHandler
''' <summary>
''' 使用证书来加密发出去的消息
''' </summary>
Dim _sslLayer As SSL.ISSLServices
Public ReadOnly Property SSLMode As Boolean
Public Sub Install(ssl As SSL.ISSLServices)
_sslLayer = ssl
_SSLMode = Not ssl Is Nothing
End Sub
''' <summary>
''' 从这个端口号进行登录协同长连接的socket正常工作的socket的端口号可以看作为UserAPI
''' </summary>
''' <returns></returns>
Public Overrides ReadOnly Property LocalPort As Integer
Get
Return Me._workSocket.LocalPort
End Get
End Property
Dim _responsehandler As DataRequestHandler
Friend Property Responsehandler As DataRequestHandler Implements IDataRequestHandler.Responsehandler
Get
Return _responsehandler
End Get
Set(value As DataRequestHandler)
_responsehandler = value
Me._workSocket.Responsehandler = AddressOf __requestHandlerInterface
End Set
End Property
''' <summary>
''' 只要是为ssl服务设置的
''' </summary>
''' <param name="remote"></param>
''' <returns></returns>
Private Function __requestHandlerInterface(CA As Long,
requestData As RequestStream,
remote As System.Net.IPEndPoint) As RequestStream
requestData = _responsehandler(CA, requestData, remote)
Return requestData
End Function
''' <summary>
'''
''' </summary>
''' <param name="LocalPort"></param>
''' <param name="OffLineMessageSendHandler">Public Delegate Sub <see cref="OffLineMessageSendHandler"/>(FromUSER_ID As <see cref="Long"/>, USER_ID As <see cref="Long"/>, Message As <see cref="RequestStream"/>)</param>
''' <param name="exHandler"></param>
Sub New(Optional LocalPort As Integer = 11000,
Optional OffLineMessageSendHandler As OffLineMessageSendHandler = Nothing,
Optional exHandler As Abstract.ExceptionHandler = Nothing)
Call MyBase.New(GetFirstAvailablePort(5000), exHandler)
Me._ProtocolHandler = New ProtocolHandler(Me)
Me.AcceptCallbackHandleInvoke = AddressOf AcceptClient
Me._workSocket = New TcpSynchronizationServicesSocket(AddressOf _ProtocolHandler.HandleRequest, LocalPort, Me.__exceptionHandle)
Me._offlineMessageSendHandler = If(OffLineMessageSendHandler Is Nothing,
Sub([from], USER_ID, MESSAGE) Call Console.WriteLine($" >>> [DEBUG {Now.ToString}, {from} => {USER_ID}] {MESSAGE}"),
OffLineMessageSendHandler)
End Sub
Public Overrides Sub Run(localEndPoint As System.Net.IPEndPoint)
Call New Thread(AddressOf Me._workSocket.Run).Start()
Call Thread.Sleep(1000)
Call $"please logon server {Me.GetType.Name} at api_port={Me._workSocket.LocalPort}".__DEBUG_ECHO
Call MyBase.Run(localEndPoint)
End Sub
Public Overrides Function Run() As Integer Implements ITaskDriver.Run
Return MyBase.Run()
End Function
''' <summary>
''' Disconnect user persistent connection who have the specific <paramref name="user_id"/> from this server.
''' (断开服务器与用户客户端的长连接)
''' </summary>
''' <param name="USER_ID">This user will be deleted from the server registry.</param>
''' <param name="removeCA">是否在删除socket句柄的时候还会删除相对应的ssl证书</param>
Public Sub DisconnectUser(USER_ID As Long, removeCA As Boolean)
If Me._socketList.ContainsKey(USER_ID) Then
Dim socket As WorkSocket = _socketList(USER_ID)
Call _socketList.Remove(USER_ID)
Call $"Clean up connection for user {USER_ID} previous connection.".__DEBUG_ECHO
Call socket.Free
End If
If removeCA AndAlso SSLMode Then
If _sslLayer.PrivateKeys.ContainsKey(USER_ID) Then
Call _sslLayer.PrivateKeys.Remove(USER_ID)
End If
End If
End Sub
Protected Overrides Sub __socketCleanup(hash As Integer)
Dim LQuery = (From usr In _socketList Where hash = usr.Value.GetHashCode Select usr.Key).FirstOrDefault
Call DisconnectUser(LQuery, True)
End Sub
''' <summary>
'''
''' </summary>
''' <param name="From"></param>
''' <param name="USER_ID"></param>
''' <param name="Message"></param>
Public Sub SendMessage(From As Long, USER_ID As Long, Message As RequestStream)
Dim request = ServicesProtocol.SendMessageRequest(From, USER_ID, Message)
Call __sendMessage(From, USER_ID, request)
End Sub
''' <summary>
''' 发送出去的数据需要进行加密假若是ssl模式的话
''' </summary>
''' <param name="From"></param>
''' <param name="USER_ID"></param>
''' <param name="Message"></param>
''' <returns></returns>
Private Function __sendMessage(From As Long, USER_ID As Long, Message As RequestStream) As RequestStream
#If DEBUG Then
Call Console.Write($"{MethodBase.GetCurrentMethod.GetFullName} {NameOf(USER_ID)}:{USER_ID} mappings to ")
#End If
USER_ID = _UidMappings(USER_ID)
#If DEBUG Then
Call Console.WriteLine(USER_ID) '衔接着上一个输出语句
#End If
If Me._socketList.ContainsKey(USER_ID) Then
Dim Socket As WorkSocket = Me._socketList(USER_ID)
Return __sendMessage(Socket, From, USER_ID, Message)
End If
#If DEBUG Then
Call $"Unable to found user '{USER_ID}' on server...".__DEBUG_ECHO
Call $"Current users: {String.Join("; ", _socketList.Keys.Select(Function(id) CStr(id)).ToArray)}".__DEBUG_ECHO
#End If
Call Me._offlineMessageSendHandler(From, USER_ID, Message)
Return NetResponse.RFC_TEMP_REDIRECT
End Function
''' <summary>
''' 将外部编号映射为内部的客户端句柄
''' 假若找不到,请返回-1
''' </summary>
''' <returns></returns>
Public Property UidMappings As Func(Of Long, Long) = AddressOf __nonUidMappings
Public Property UidMappingsBack As Func(Of Long, Long) = AddressOf __nonUidMappings
Private Shared Function __nonUidMappings(USER_ID As Long) As Long
Return USER_ID
End Function
''' <summary>
'''
''' </summary>
''' <param name="socket"></param>
''' <param name="From">这个是这一条消息的源头,可能需要进行映射</param>
''' <param name="USER_ID"></param>
''' <param name="Message"></param>
''' <returns></returns>
Private Function __sendMessage(socket As WorkSocket,
From As Long,
USER_ID As Long,
Message As RequestStream) As RequestStream
From = Me._UidMappingsBack(From)
If socket Is Nothing OrElse socket.workSocket Is Nothing Then
Call _socketList.Remove(USER_ID)
Call Me._offlineMessageSendHandler(From, USER_ID, Message)
Return NetResponse.RFC_TEMP_REDIRECT
End If
#If DEBUG Then
Call Console.WriteLine($"*Call {NameOf(__sendMessage)} ===> {Message}")
#End If
If SSLMode Then
Dim CA As SSL.Certificate = _sslLayer.PrivateKeys(USER_ID)
Dim post As New SendMessagePost(Message.ChunkBuffer)
post.Message = CA.Encrypt(post.Message)
post.FROM = From
Message = New RequestStream(ServicesProtocol.ProtocolEntry,
ServicesProtocol.Protocols.SendMessage,
post.Serialize)
#If DEBUG Then
Call $"Request encrypts from {CA.uid} job done!".__DEBUG_ECHO
#End If
End If
Call socket.SendMessage(Message) '原封不动的进行数据转发
Return NetResponse.RFC_OK
End Function
''' <summary>
''' 用户客户端请求发送消息至指定编号的用户的终端之上
''' </summary>
''' <param name="CA"></param>
''' <param name="request"></param>
''' <param name="remote"></param>
''' <returns></returns>
<Protocol(ServicesProtocol.Protocols.SendMessage)>
Private Function __usrInvokeSend(CA As Long, request As RequestStream, remote As System.Net.IPEndPoint) As RequestStream
Dim [From] As Long, USER_ID As Long
#If DEBUG Then
Call $"{NameOf(From)}:{From} invoke send to {NameOf(USER_ID)}:{USER_ID}".__DEBUG_ECHO
#End If
If ServicesProtocol.GetSendMessage(request, From, USER_ID) Then
Return Me.__sendMessage(From, USER_ID, request)
Else
Return NetResponse.RFC_TOKEN_INVALID
End If
End Function
<Protocol(ServicesProtocol.Protocols.Logon)>
Private Function __Logon(CA As Long, request As RequestStream, remote As System.Net.IPEndPoint) As RequestStream
Dim USER_ID As Long, remoteEp As String = ""
If Not ServicesProtocol.GetLogOnUSER(request.GetUTF8String, USER_ID, remoteEp) Then
Return NetResponse.RFC_TOKEN_INVALID
End If
Dim hash As Integer = CInt(Val(remoteEp))
If Not Me._Connections.ContainsKey(hash) Then
Call Console.WriteLine($"No connection could be made! {hash} for socket hash is not exists in the hash table!")
Return NetResponse.RFC_CONFLICT
End If
Dim SocketClient As WorkSocket = Nothing
Call Me._Connections.TryGetValue(hash, SocketClient)
If SocketClient Is Nothing Then
Call Console.WriteLine("No connection could be made!")
Return NetResponse.RFC_BAD_GATEWAY
Else
Call Console.WriteLine(" >> " & SocketClient.workSocket.RemoteEndPoint.ToString)
End If
Call DisconnectUser(USER_ID, False)
Call _socketList.Add(USER_ID, SocketClient)
Return NetResponse.RFC_OK
End Function
<Protocol(ServicesProtocol.Protocols.Broadcast)>
Private Function __broadcastMessage(CA As Long, request As RequestStream, remote As System.Net.IPEndPoint) As RequestStream
For Each cnn In Me.Connections
Call cnn.SendMessage(request)
Next
Return NetResponse.RFC_OK
End Function
<Protocol(ServicesProtocol.Protocols.GetMyIPAddress)>
Private Function __getMyIPAddress(CA As Long, request As RequestStream, remote As System.Net.IPEndPoint) As RequestStream
Return New RequestStream(0, HTTP_RFC.RFC_OK, remote.ToString.Split(":"c)(Scan0))
End Function
<Protocol(ServicesProtocol.Protocols.ConfigConnection)>
Private Function __isGetSocketPortal(CA As Long, request As RequestStream, remote As System.Net.IPEndPoint) As RequestStream
Return New RequestStream(0, HTTP_RFC.RFC_OK, CStr(_LocalPort))
End Function
<Protocol(ServicesProtocol.Protocols.IsUserOnline)>
Private Function __isUserOnlineQuery(CA As Long, request As RequestStream, remote As System.Net.IPEndPoint) As RequestStream
Dim USER_ID As Long = Scripting.CTypeDynamic(Of Long)(request.GetUTF8String)
Dim result As String = CStr(Me._socketList.ContainsKey(USER_ID))
Return New RequestStream(0, HTTP_RFC.RFC_OK, result)
End Function
Dim _freeCnnInfo As New List(Of String)
''' <summary>
''' 哈希值不存在于现有的登录用户列表之中就是空闲连接
''' </summary>
Public Sub RemoveFreeConnections()
Dim LQuery = (From Guid As String In _freeCnnInfo.AsParallel '上一次刷新的时候的空闲连接
Let LowCnn = (From cnn In Me.Connections.AsParallel
Where Guid.Equals(CStr(cnn.GetHashCode))
Select cnn).FirstOrDefault
Where Not LowCnn Is Nothing AndAlso
(From cnn In Me._socketList
Where Guid.Equals(CStr(cnn.Value.GetHashCode))
Select cnn).ToArray.IsNullOrEmpty '哈希值不存在的
Select LowCnn).ToArray '对于上一次刷新的列表之中的连接而言,假若在这么长的一段时间间隔之中还是处于空闲状态,则服务器会将这些连接断开连接
For Each cnn In LQuery
Call Me.ForceCloseHandle(cnn)
Call cnn.Free
Next
Call $"Clean up {LQuery.Length } free connections.....".__DEBUG_ECHO
'获取新产生的空闲连接
_freeCnnInfo = (From cnn In Me._Connections.AsParallel
Where (From item In Me._socketList Where item.Value.GetHashCode = cnn.GetHashCode Select 1).ToArray.IsNullOrEmpty
Select Guid = CStr(cnn.GetHashCode)).AsList
Call $"{_freeCnnInfo.Count} free connections pending for clean up....".__DEBUG_ECHO
For Each usr In Me._socketList.ToArray
Call usr.Value.SendMessage(NetResponse.RFC_OK)
Next
End Sub
''' <summary>
''' 建立一个新的连接
''' </summary>
''' <param name="Client"></param>
Private Sub AcceptClient(Client As WorkSocket)
'Do Nothing
Call $"{Client.workSocket.RemoteEndPoint.ToString} connection request accept!".__DEBUG_ECHO
End Sub
Public Iterator Function GetEnumerator() As IEnumerator(Of KeyValuePair(Of Long, WorkSocket)) Implements IEnumerable(Of KeyValuePair(Of Long, WorkSocket)).GetEnumerator
For Each entry In Me._socketList.ToArray
Yield entry
Next
End Function
Private Iterator Function IEnumerable_GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
Yield GetEnumerator()
End Function
Protected Overrides Sub Dispose(disposing As Boolean)
If disposing Then
For Each cnn In Me._socketList.ToArray
Call DisconnectUser(cnn.Key, True)
Next
End If
Call MyBase.Dispose(disposing)
End Sub
End Class
End Namespace