I'm creating an instance of clsSocketWrapper and connecting it to uswest.battle.net:6112. It's telling me that the connection timed out, when it shouldn't have. It makes me cry.
clsSocketWrapper:
Imports System.Text
Imports System.Net
Imports System.Net.Sockets
Public Class clsSocketWrapper
Private MySocket As Socket
'/**
' Connects this socket
' @param server Server to connect to
' @param port Port to connect on
' @return True if connected, false if not.
'*/
Public Function Connect(ByVal server As String, ByVal port As Integer) As Boolean
Randomize()
Dim hostEntry As IPHostEntry = Dns.GetHostEntry(server)
Dim address As IPAddress = hostEntry.AddressList(Int(Rnd() * hostEntry.AddressList.Length))
Dim endPoint As New IPEndPoint(address, port)
Dim tempSocket As New Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
Try
tempSocket.Connect(endPoint)
Catch ex As Exception
modFunctions.AddChat(Color.Red, "Error: " & ex.ToString)
End Try
If tempSocket.Connected Then
MySocket = tempSocket
Return True
End If
Return False
End Function
'/**
' Disconnects the socket
'*/
Public Sub Disconnect()
MySocket.Disconnect(True)
End Sub
'/**
' Checks if the socket is connected.
' @return True if connected, false if not.
'*/
Public Function IsConnected() As Boolean
Return MySocket.Connected
End Function
'/**
' Receives data
' @return All the incomming data on the socket
'*/
Public Function ReceiveData() As String
Dim bytesReceived(255) As Byte
Dim bytes As Integer
Dim data As String = ""
Do
bytes = MySocket.Receive(bytesReceived, bytesReceived.Length, 0)
data = data & Encoding.ASCII.GetString(bytesReceived, 0, bytes)
Loop While bytes > 0
ReceiveData = data
End Function
'/**
' Sends data
' @param b Data to send
' @return True if sent, false otherwise.
'*/
Public Function SendData(ByVal b As Byte()) As Boolean
Return MySocket.Send(b, b.Length, 0)
End Function
'/**
' Overload of SendData(byte())
'*/
Public Function SendData(ByVal s As String) As Boolean
Dim b(Len(s) - 1) As Byte, I As Integer
For I = 1 To Len(s)
b(I - 1) = CType(Mid(s, I, 1), Byte)
Next
Return SendData(b)
End Function
End Class
Usage:
Public Class clsStarcraftPacketThread
Private MySocket As New clsSocketWrapper
Private PacketBuffer As New clsPacketBuffer
Private Active As Boolean
Public Sub StartThread()
modFunctions.AddChat(Color.Yellow, "Connecting to Battle.net server " & My.Settings.Server & ":6112..")
If MySocket.Connect(My.Settings.Server, 6116) Then
modFunctions.AddChat(Color.GreenYellow, "Connected to Battle.net!")
Active = True
Else
modFunctions.AddChat(Color.Red, "Failed to connect to Battle.net!")
Active = False
End If
If Active Then
SendPacket_0x50()
modFunctions.AddChat(Color.Yellow, "Requesting authorization..")
End If
End Sub
The bot yelling at me:
Quote[3:49:43 AM] Welcome to Magnicient Bot by Joe[e2]!
[3:49:46 AM] Connecting to Battle.net server uswest.battle.net:6112..
[3:50:07 AM] Error: System.Net.Sockets.SocketException: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond
at System.Net.Sockets.Socket.DoConnect(EndPoint endPointSnapshot, SocketAddress socketAddress)
at System.Net.Sockets.Socket.Connect(EndPoint remoteEP)
at MagnificentBot.clsSocketWrapper.Connect(String server, Int32 port) in C:\Users\Joe\Documents\Visual Studio 2005\Projects\MagnificentBot\MagnificentBot\clsSocketWrapper.vb:line 27
[3:50:07 AM] Failed to connect to Battle.net!
Also, can anyone think of a better name? Magnificent Bot sounds *really* stupid.
OK first of all, dump the "cls" prefix. Notice that nowhere else in the world of .NET is a class prefixed with "cls".
I'll be back in a bit to answer more!
Done. Proceed.
I was looking at your starcraft packet thread "class". I think it's a mistake, or poor design, to design a class simply to handle the one specific client, particularly when the implementation on the other clients is not particularly different.
What you should do for your background thread is implement *only* the listening portion of your socket. Now, I should warn you: I've always found .NET sockets to be a bit odd in behavior for receiving. For example, they don't always fail if they disconnect, and sometimes tell you that it got data when it really didn't.
Having said so, the following code follows the patterns I used when designing my WoW realm server client. It has not been tested or even run through the IDE (so I don't know if it's syntactically correct). But I commented it a bunch, and you should understand what each subroutine does.
Imports System.Text
Imports System.Net
Imports System.Net.Sockets
Imports System.Threading
Public Class SocketWrapper
Private sck As Socket
' An example of using Hungarian notation to avoid confusing the thread for a list
Private m_tdList As Thread
Private m_connected As Boolean
Public Sub New()
sck = New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
m_tdList = New Thread(AddressOf Listener)
m_tdList.IsBackground = True
m_tdList.Priority = Priority.BelowNormal
m_connected = False
End Sub
Public Function Connect(ByVal ep As IPEndPoint) As Boolean
Dim connected As Boolean = False
Try
sck.Connect(ep)
m_connected = connected = sck.Connected
Catch se As SocketException
' Inform the user of the error (your code)
m_connected = connected = False
End Try
' Note: there are other exceptions that could have been caught here, but
' it is more appropriate for them to be caught up the food chain. For example,
' the main application should catch a SecurityException, or the class that uses
' this one should catch an ObjectDisposedException.
Return m_connected
End Function
Public Sub BeginListening()
If Not IsConnected Exit Sub
m_tdList.Start()
End Sub
Public Sub Close()
If Not IsConnected Exit Sub
Try
If sck <> Nothing Then sck.Close() : sck = Nothing
If m_tdList <> Nothing Then m_tdList.Abort() : m_tdList = Nothing
Catch tae As ThreadAbortException
' No code is necessary here.
Finally
m_connected = False
End Try
End Sub
Public Readonly Property IsConnected As Boolean
Get
Return m_connected
End Get
End Property
Public Readonly Property IsReceiving As Boolean
Get
If m_tdList = Nothing Then Return False
Return m_tdList.IsAlive
End Get
End Property
' This subroutine implements a general Battle.net protocol listener
' It does only header parsing.
Private Sub Listener()
Do While sck.Connected
Dim packetId As Byte
Dim dataLength As Short
Dim packetData() As Byte
Try
Dim header() as Byte
header = Receive(4)
If header(0) <> &Hff Then
' Notify user of protocol violation (your code)
' Break out of the loop which ends the thread
sck.Close()
Exit Do
End If
packetId = data(1)
dataLength = BitConverter.ToInt16(header, 2)
packetData = Receive(dataLength)
Catch se As SocketException
' Notify the user of the error (your own code here)
' End the loop, which ends the thread
Exit Do
End Try
' Now we have the variables packetId and dataLength, and packetData filled in.
' Call your custom parsing method.
' Alternatively, add your packet data to a priority queue (with its ID)
' Then, on yet another thread, take the highest-priority packets out first
' and process them as you have time. In this case, I just do a direct parse
' on the listener thread. The problem with this is that if there is an
' unhandled exception, it can terminate the listening thread. So, give it
' its own Try/Catch block.
Try
Parse(packetId, packetData)
Catch ex As Exception
' Inform user of the error with the packet ID and data, but
' this time we don't end the loop.
End Try
Loop
End Sub
Protected Overridable Function Receive(ByVal num As Integer) As Byte()
Dim data(num) As New Byte
Dim curIndex As Integer = 0
Dim toGo As Integer = num
While toGo > 0
Dim tmpLen As Integer
tmpLen = sck.Receive(data, curIndex, toGo, SocketFlags.None)
curIndex = curIndex + tmpLen
toGo = toGo - tmpLen
Loop
Return data
End Function
End Class
Some notes:
After calling Close(), your object will be invalid, so you'll need to create a new one for a new connection.
You should call Close() whenever you have an error. You can do this from an external object (through events), or within your code in places I marked "your code goes here".
Be aware that not all exceptions are caught.
Any questions, feel free to ask. :)
Alright. I'll look into that tomorrow.
One thing I need to caution you about using this method:
Having the listening happening on the background thread in a loop is great because the Receive() method (Socket.Receive()) blocks thread execution until data is received, which could mean that, had you done it on your main thread, nothing on your window would be redrawn.
The price you pay, though, is having to marshal your GUI updates to the main thread, because Windows controls can only change their appearance on the main thread.
I would suggest doing this. Whatever your current AddChat routine is, rename it to AddChatImpl (for add chat implementation). Create a new AddChat with the same signature.
Let's say your AddChat routine is this:
Public Sub AddChat(ByVal ParamArray obj() As Object)
...
End Sub
Declare this. It's called a delegate, and it's like a function pointer in C and C++, but you know that it's type safe and that it's definitely a function pointer and not a pointer to somewhere on the heap:
Public Delegate Sub AddChatCallback(ByVal ParamArray obj() As Object)
Note how the delegate's signature matches your AddChat signature.
Now, in your new AddChat:
Public Sub AddChat(ByVal ParamArray obj() As Object)
Dim callback As New AddChatCallback(AddressOf AddChatImpl)
If rtbChat.InvokeRequired Then
rtbChat.Invoke(callback, obj)
Else
callback(obj)
End If
End Sub
There's one other thing. Your AddChatImpl signature - and the delegate - should not be ParamArray. .NET gets weird because the runtime isn't sure whether you are passing the array as the array, or as item 0 of the array (since an array is also just another object).
So, all told, your new code should look like this:
Private Delegate Sub AddChatCallback(ByVal obj() As Object)
Private Sub AddChatImpl(ByVal obj() As Object)
' This used to be the AddChat function.
End Sub
Public Sub AddChat(ByVal ParamArray obj() As Object)
Dim callback As New AddChatCallback(AddressOf AddChatImpl)
If rtbChat.InvokeRequired Then
rtbChat.Invoke(callback, obj)
Else
callback(obj)
End If
End Sub
You'll need to do this kind of thing for anything that you want to update your GUI for if it will have to do with data you got from Battle.net. So if you get friends or a clan list, for instance, adding those to ListViews will likely also need to have the Invoke call.