Jump to content
bc-vnt

Building a UDP Client/Server application in VB.NET

Recommended Posts

Introduction

If you're new to UDP, I suggest you reading UDP Send and Receive using threads in VB.NET - CodeProject by Kumudu Gunasekara. I used his work as a take-off point, and wish to thank him. There's also a TinyUDP - Simple UDP Client/Server Components for .NET - CodeProject article on Code Project, which also looks very promising. What I'm doing here is going a bit more in depth, to show a possible way of UDP communication.

As you may already heard UDP is well-know as "unreliable" protocol. Unreliable means that you have no guarantees that the packets you send will ever reach their destination. UDP also doesn't guarantees that individual packets of a transmission will arrive in the same order they were sent. There is also a problem of duplicate messages (you sent 1 and 3 arrived, woah!). If any kind of reliability for the information transmited is needed, it must be implemented in upper layers - i.e. in your application.

So your first task as UDP coder is to design a low-level protocol, which will be handling datagram transfers for you. Two things it must do: ensure delivery and ensure delivery-in-correct-order. On top of that protocol you may later add a protocol for actual communications (i.e. chat message commands, nickname changes, etc).

I do not recommend you to use my code in your real-world client/server applications. Just see how it works (not too good at times!) and build a better one.

Protocols

As mentioned before, this design uses 2 protocols, low-level or delivery protocol for datagram transmissions and and high-level or actual protocol for application communications. Both protocols benefit from BinNumerization proccess, which I must explain before we go on.

BinNum, UnBinNum and packet delimiting

Imagine a task: transfer 2 vairables A and B (they containt numbers 34 and 257) over a network. What are the possible ways to do this?

ASCII.

Encode like this (most common way): 34|257

Numbers are delimited by a special ASCII character, called.. delimiter. Some times CrLf is used for such purpose. The main downfall of this method is a guy, who comes to your chat with a name like 'TheE||vis' . And even more serious problem lies in a sphere of file transfer. Binary files tend to use all kind of bytes in them, you know.

Delimiting is the most obious way to do things, and because of that, I strongly recommend avoiding it.

Encode like this (sometimes used in ASCII protocols): 0003400257

Each number uses 5 digits to represent itself and adds trailing zeros. This way, dividing the string into 2 parts will give you 00034 and 00257. Downfalls: you lose some bytes on stupid zeros.

Encode like this : 2.343.257

What's actually going on here is a length of string representing your number delimited from the actual number with an ASCII character. This way, parser reads up everything you need before the dot (will get "2"), then eats out exactly 2 characters ("34"), which will leave him with a "3.257" string, and all he has to do is.. repeat.

By the way, if you're developing a protocol, consider the above scheme! This is ofcourse more imortant on TCP, where the packets are streamed and cutting/fitting chunks is a number one priority.

Binary.

That's easy. Each number is encoded as a byte. Or as a word. This is almost a perfect way to transfer data. The only downfall of which is: I can encode 34 into a byte, but have no way to fit 257 into a byte, so, I have to use a word for it. If variable B (257) becomes 255 someday, it will spare 3 extra bytes for it's word encoding.

BinNumerization.

This method requires some work on the sending and receiving sides, but NEVER spares even a single byte with unnessecery data. The number is encoded as byte, if it is below 248 and as a string with it's length added if it's above. 255-248 = 7 digit numbers maximum. You can adjust those values for your needs. But be sure to do this on both sides!

Public Function UnBinNum(ByRef S$, Optional ByVal EatOut As Boolean = True) As Integer
'MsgBox("CALL FOR UNBIN NUM:" & S$)
Dim l As Int16
Dim nval As Long
Dim h As String
'On Error GoTo Error
If Asc(Left(S$, 1)) < 249 Then
nval = Asc(Left(S$, 1))
If EatOut = True Then S$ = Right$(S$, Len(S$) - 1)
Else
l = Asc(Left$(S$, 1)) - 248
h = Mid$(S$, 2, l)
If Len(S$) < l + 1 Then nval = -1 : GoTo ErrorS
'Debug.Print "len: " & l
'Debug.Print "hex:" & h
'Debug.Print "unhex:" & HexToDecimal(h)
nval = HexToDecimal(h)
If EatOut = True Then S$ = Right$(S$, Len(S$) - l - 1)
End If
ErrorS:
'MsgBox("nval:" & nval)
Return CInt(nval)
End Function

Function BinNum(ByVal NUM) As String
Dim h As String, l As Byte
If NUM > 248 Then
h = Hex(NUM)
l = Len(h) + 248
If l > 255 Then MsgBox("L:" & l & "...." & h & "..." & Len(h))
Return (Chr(l) & h)
Else
Return (Chr(NUM))
End If
End Function

Yeah, I use those 2 in VB6 too. They also rely on HexToDecimal function found in the source (written not by me). Also note, that UnBinNum takes it's argument ByRef!Anyways, this implementation is not perfect (one of the downfalls is that you can't use negative numbers), but should give you a fair idea and prove, that the concept IS perfect. Well, at least almost: you still lose some bytes when encoding numbers in range 249-255 (they are encoded as strings, not as bytes).

LOW-Level Delivery Protocol

Each packet begins with 2 bytes and 1 BinNum representing the ClientID, PacketType and Sequence. The rest is a HIGH-level data and shouldn't be used on a low level.

All three are vital. Since there is no way to determine a single connection in UDP (all you have to deal with is: IP, PORT, DATA that came) clientID comes in place.

On my server example IP+ClientID form a unique client, while IP+Port are only used while there's no ClientID at all. But that happens only during the handshake.

Handshake

Client begins to transmit the NIL packet (made of three empty bytes, or 2 empty bytes and 1 empty BinNum, which is the same at the moment). Basicly client is saying:

I'm new client, no ClientID, no transmission history, no previous experience, please give me a ClientID, I could work with.

The server assigns a new ClientID to a pair of IP+port, and sends a welcoming message (one from the high level protocol).

PacketTypes & Delivery

The most usefull PacketType is INF (byte 2). It suggests a real data inside. With each INF received, receiver should send back the packet with type ACK (byte 0) or BUF (byte 1) which mean basicly the same: PACKET RECEIVED, DO NOT RESEND.

The sender keeps sending the INF from the queue until it gets an ACK (or a BUF). What is the differnce between ACK and BUF you ask? BUF is sent whenever the packet is out of sequence (too old or came from the future), and might be not implemented by your client (if you have no buffering mechanism), but the receiver MUST ACK every incoming packet.

'Sending side

Function Compose(ByVal RAWDATA as String)
'increment counter
outSeq += 1
'add header
RAWdata = Chr(clientID) & Chr(typ) & BinNum(outSeq) & RAWdata

Dim dlg As New UDPMaster.DGram
dlg.IP = IP
dlg.Port = port
dlg.data = UDPMaster.StringToBytes(RAWdata)

'add to sending buffer
sendBuffer.Add(dlg)
End Function

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' the sending side then loops through sendbuffer and sends it
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

'Receiving side

Function CollectData()
' read Header
' byte(0) - clientID, byte(1) - type, UnBinNum (seqNum)

If seqNum = mCl.inSeq Then ' ONE WE WAITED FOR
udp.Send(mCl.IP, mCl.port, mCl.ComposeACK)
'ACK immidiatly
'increment the counter
mCl.inSeq += 1
End if
End Function

'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' the sending side now receives an ACK and removes packet
' from sendBuffer
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

There is also a 255 or TERMINATION PacketType, which is sent upon disconnection. It is not mandatory, cause UDP is connection-less, and applications usually know their ways to tell disconnected clients (i.e. long time without a single ACK, or a simple ping time out).

Each packet you queue to send is assigned a Sequnence number, which increases with every such action. Your ClientID as also packed inside the header as the first byte. The PacketType is INF, ACK or BUF. Only INF packets contan actual data, the rest are for signaling and managment.

Reconnection.

Server could send a NIL packet to the client, which means, that they have to perform a handshake again. This option should be used after a long disconnection time.

HIGH-level actual protocol

Nothing fancy here. It's mostly ASCII, with simple uppercased words as commands. Availible commands are "WHO" to list all connected clients, "SAY text" to transmit said text to everyone, and "FILE filename" to request a file transfer.

The Code

I've extracted the most generic bit to a UDPMaster class, which can serve both as a client and a server. UDPMaster also contains DGram class which is a structure to hold incoming datagrams.

Here's how you use it to make a client (OR! a server).

Dim udp as new UDPMaster(2002) ' local port you are listening too

Do
If udp.hasnews then
'Receive data
Dim dgram as UPDMaster.Dgram
udp.poll(dgram)
'Show data
Console.WriteLine ("DATAGRAM received ") ;
Console.WriteLine ("Sender: " & dgram.IP & ":" & dgram.port)
'the data is holded in dgram.data() byte array
'Work with data...
' ...........
end if
'Send data...
udp.send(drgam.IP, dgram.port, "REPLY")
Loop

Both client and the server are built on top of that code, but that's it. The UDPMaster does not contain any of the protocol specific features described above. It just reports new datagrams (or sends yours). Working with packets, deciding which goes to where, buffering them and acknowledging - all that is done on a higher level.

The other class shared by both programs is client class. No particular reason for this, it was just convnient to use: this class stores IP, data, ping times etc, of 1 given client, so instead of declaring all those variables on the client side, I just stole the class from the server and declared a single instance. This makes files UDPMaster.vb and Helper.vb, found in both sollutions, identical.

I've left alot of comments, but they more point to something, then explain anything (my comment righting skills are very low). To get a better understanding of what is going on you may uncomment all of those <code>BetCon</code> lines, they do the reporting to the console.

Pressure control

Only implement at server and should ONLY be seen as TEST-DEMO-DIRECTION-ETC. The pressure parameter (each client has one) is an ammount of ticks (1/1000 of second) the server will wait for ACK before retransmitting the data. Lowering this value will increase the pressure and the ammount of packets sent.

Slow start

I've tried to implement a slow start method (as seen in TCP), but with not much luck. The default pressure is 1000, which is then reduced to a suitable value, depending on packetloss.

Packetloss

The calculation of packetloss is made every 10 packet sends. The server divides ammount of sent packets to the ammount of received ACKs. If packet loss is high it will reduce pressure, and vice versa.

Round trip times

Time it takes for a packet to travel to it's destination and back. Calcualted by 2 timestamps (sent, received). This value should be aproximated (that's what gurus suggest), and that's exactly what those commented lines of code do (found in Done method of Client class)

I hope, you'll be able to build a desent pressure control system, cause I kinda failed on that task. If you're really up to this task, try reading TCP/IP RFCs, they provide alot of intersting techniques.

Download source

Download project demo

http://www.codeproject.com/Articles/13935/Building-a-UDP-Client-Server-application-in-VB-NET

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...