Вы не ожидаете, что элемент управления Winsock действительно отправит сообщение "quit". Метод SendData
является асинхронным: он может вернуться до того, как данные действительно будут отправлены по сети. Данные буферизуются локально на вашем компьютере и через некоторое время отправляются сетевым драйвером.
В вашем случае вы пытаетесь отправить сообщение "выход", а затем почти сразу после этого закрываете сокет. Поскольку SendData
является асинхронным, вызов может вернуться до того, как на сервер будет отправлено сообщение «quit», и, следовательно, код может закрыть сокет, прежде чем он сможет отправить сообщение.
Это работает, когда вы сначала отменяете выгрузку формы и позволяете таймеру отправить сообщение «выход», потому что вы даете сокету достаточно времени, чтобы отправить сообщение на сервер, прежде чем сокет закроется. Тем не менее, я бы не рассчитывал на это всегда работает; это совпадение, что дополнительные шаги дали сокету достаточно времени для отправки сообщения, и не всегда так получается.
Вы можете устранить проблему, подождав, пока сокет вызовет событие SendCompleted
после отправки сообщения «quit» и до закрытия сокета. Ниже приведен основной пример. Обратите внимание, что код QueryUnload
намного проще.
Private m_bSendCompleted As Boolean
Private m_bSocketError As Boolean
Private Sub singleSock_Error(ByVal Number As Integer, Description As String, ByVal Scode As Long, ByVal Source As String, ByVal HelpFile As String, ByVal HelpContext As Long, CancelDisplay As Boolean)
'Set error flag so we know if a SendData call failed because of an error'
'A more robust event handler could also store the error information so that'
'it can be properly logged elsewhere'
m_bSocketError = True
End Sub
Private Sub singleSock_SendCompleted()
'Set send completed flag so we know when all our data has been sent to the server'
m_bSendCompleted = True
End Sub
'Helper routine. Use this to send data to the server'
'when you need to make sure that the client sends all the data.'
'It will wait until all the data is sent, or until an error'
'occurs (timeout, connection reset, etc.).'
Private Sub SendMessageAndWait(ByVal sMessage As String)
m_bSendCompleted = False
singleSock.SendData sMessage
singleSock.SendData sMessage
Do Until m_bSendCompleted or m_bSocketError
DoEvents
Loop
If m_bSocketError Then
Err.Raise vbObjectError+1024,,"Socket error. Message may not have been sent."
End If
End Sub
Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
'This is (almost) all the code needed to properly send the quit message'
'and ensure that it is sent before the socket is closed. The only thing'
'missing is some error-handling (because SendMessageAndWait could raise an error).'
If UnloadMode = vbFormControlMenu Then
Me.WindowState = vbMinimized
Cancel = True
Else
SendMessageAndWait "quit" & vbCrLf
singleSock.Close
End If
End Sub
Вы можете сделать код чище, добавив логику для отправки сообщения и дождавшись его отправки в отдельный класс. Это хранит приватные переменные и обработчики событий в одном месте, вместо того, чтобы они засоряли ваш основной код. Это также облегчает повторное использование кода, когда у вас есть несколько сокетов. Я назвал класс SynchronousMessageSender
из-за отсутствия лучшего имени. Этот пример также имеет более полную обработку ошибок:
SynchronousMessageSender.cls
Private WithEvents m_Socket As Winsock
Private m_bAttached As Boolean
Private m_bSendCompleted As Boolean
Private m_bSocketError As Boolean
Private Type SocketError
Number As Integer
Description As String
Source As String
HelpFile As String
HelpContext As Long
End Type
Private m_LastSocketError As SocketError
'Call this method first to attach the SynchronousMessageSender to a socket'
Public Sub AttachSocket(ByVal socket As Winsock)
If m_bAttached Then
Err.Raise 5,,"A socket is already associated with this SynchronousMessageSender instance."
End If
If socket Is Nothing Then
Err.Raise 5,,"Argument error. 'socket' cannot be Nothing."
End If
Set m_Socket = socket
End Sub
Private Sub socket_SendCompleted()
m_bSendCompleted = True
End Sub
Private Sub socket_Error(ByVal Number As Integer, Description As String, ByVal Scode As Long, ByVal Source As String, ByVal HelpFile As String, ByVal HelpContext As Long, CancelDisplay As Boolean)
m_bSocketError = True
'Store error information for later use'
'Another option would be to create an Error event for this class'
'and re-raise it here.'
With m_lastSocketError
.Number = Number
.Description = Description
.Source = Source
.HelpFile = HelpFile
.HelpContext = HelpContext
End With
End Sub
'Sends the text in sMessage and does not return'
'until the data is sent or a socket error occurs.'
'If a socket error occurs, this routine will re-raise'
'the error back to the caller.'
Public Sub SendMessage(ByVal sMessage As String)
If Not m_bAttached Then
Err.Raise 5,,"No socket is associated with this SynchronousMessageSender. Call Attach method first."
End If
m_bSendCompleted = False
m_bSocketError = False
m_socket.SendData sMessage & vbCrLf
'Wait until the message is sent or an error occurs'
Do Until m_bSendCompleted Or m_bSocketError
DoEvents
Loop
If m_bSocketError Then
RaiseLastSocketError
End If
End Sub
Private Sub RaiseLastSocketError()
Err.Raise m_lastSocketError.Number, _
m_lastSocketError.Source, _
m_lastSocketError.Description, _
m_lastSocketError.HelpFile, _
m_lastSocketError.HelpContext
End Sub
Пример использования
Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
Dim sender As New SynchronousMessageSender
'Ignore errors since the application is closing...'
On Error Resume Next
If UnloadMode = vbFormControlMenu Then
Me.WindowState = vbMinimized
Cancel = True
Else
Set sender = New SynchronousMessageSender
sender.AttachSocket singleSock
sender.SendMessage "quit"
singleSock.Close
End If
End Sub
Используя отдельный класс, теперь весь необходимый код можно поместить в Form_QueryUnload
, что поддерживает порядок.