Это первая часть серии руководств, в которой будет описана поддержка сокетов в WP7 с точки зрения разработчика. Эта серия учебных пособий будет посвящена разработке приложений WP7, для которых требуется длительное TCP-соединение для отправки и получения текстовых данных. В этих постах мы рассмотрим создание полноценного IRC-клиента.
Основное внимание в этих постах уделяется разработчикам WP7. Если вы хотите лучше понять внутренние детали интернет-сокетов, в Википедии есть хорошее введение по этому вопросу.
В этом первом посте мы рассмотрим, как можно создать и открыть TCP-соединение с сервером.
Фон
Скоро появится обновление Mango для Windows Phone 7, и одной из новых функций является добавленная поддержка сокетов. WP7 Mango поддерживает протоколы TCP и UDP и, к радости многих, его диапазон портов не был ограничен. Таким образом, устройства Mango могут подключаться к любому IP-адресу и к любому порту, который они хотят, если он открыт.
Пространство имен System.Net.Sockets
Все необходимые компоненты для создания успешного TCP-соединения находятся внутри System.Net.Sockets –namespace. Пространство имен содержит более 25 классов и перечислений, но из этих 25 только два класса выделяются как неотъемлемая часть поддержки сокетов: класс Socket и класс SocketAsyncEventArgs . Понимание этих двух классов является ключом к пониманию поддержки сокетов.
К сожалению, пространство имен System.Net.Sockets — странная птица, когда дело доходит до соглашений об именах и удобстве использования. Иногда я удивлялся, как одно пространство имен может так сильно отличаться от других. Может быть, тот факт, что он имеет дело с сокетами, заставил все пространство имен изменить правила. Возможно, у меня просто не было необходимости использовать те аспекты поддержки сокетов, которые заставили классы внутри пространства имен работать странным образом. Но из того, что я лично видел и испытал на уроках, вся поддержка сокетов могла бы быть разработана иначе. И в этом случае «по-другому» означает, что вместо того, чтобы придумывать новое значение для событий и аргументов событий, он мог бы придерживаться руководства.
We will focus only in these two classes because they are the central piece when creating our demo-application, the IRC-client. But when we start creating our application, we will cover most of the other interesting classes in the namespace.
The Socket-class
An instance of the socket-class represents our connection to the server. This instance will define the key aspects of the connection, like the selection between the TCP and the UDP-protocols. In our case we want to create the connection and keep it open. So we create the instance, connect to the server using the ConnectAsync-methodand then keep the socket-instance available for further work.
The socket-class offers the basic operations like ConnectAsync, SendAsync and ReceiveAsync. What makes it little complicated is that it doesn’t provide any feedback by itself if these operations succeeded or not. So you execute your work through the socket-class but you receive the feedback through a different route: With the help of the SocketAsyncEventArgs-class.
The SocketAsyncEventArgs-class
Where the socket-class comes short is providing any information on the events that happen with our connection. Instead we have to always rely on the SocketAsyncEventArgs-class to provide us the information we want. And even though the class has the “EventArgs”-postfix, it is up to you to create the instances of this class.
When you execute an operation with the Socket-class, you’ll have to provide the SocketAsyncEventArgs-instance for it as a parameter. The SocketAsyncEventArgs-instance will always raise a Completed-event when the operation has been executed. So, instead of providing the ConnectionCompleted, MessageReceived and MessageSent-events, you will always receive the Completed-event which you must know how to handle.
To differentiate between the different operations, the SocketAsyncEventArgs-class provides a LastOperation-property which is of type SocketAsyncOperation. This enum contains values like “Connect”, “Receive” and “Send”.
How to use the Socket-class and the SocketAsyncEventArgs-class
As mentioned above, you work with the Socket-class by creating an instance of SocketAsyncEventArgs and passing it as an parameter for the socket operation you want to execute. The SocketAsyncEventArgs will always raise the Completed-event when the operation has finished.
Here’s some pseudo-code which hopefully will make this more clear:
var myConnection = new Socket(); var socketOperationEventArguments = new SocketAsyncEventArgs(); socketOperationEventArguments.Completed += OnConnectionCompleted; myConnection.ConnectAsync(socketOperationEventArguments);
When the operation has finished, no matter what operation you executed, the SocketAsyncEventArgs will always raise a Completed-event. So, if you create a connection or receive a message or send a message, you will always receive the Completed-event. If you are re-using your SocketAsyncEventArgs-instances, like the MSDN-tutorial shows, you must always check the instance’s LastOperation-property for its current value and only then you will know if you received a new message or if the event was actually feedback for the “message send” operation. Fortunately there’s a better way.
The practical way for working with the Socket-class and the SocketAsyncEventArgs-class
If you’re familiar with the NHibernateyou may know that to get it up and running you create one instance of SessionFactory and keep hold of it. Then you create a new Session when ever you need to access the database. When your work with the DB is done, you get rid of the Session.
My personal recommendation is to treat the Socket and the SocketAsyncEventArgs classes in a similar fashion: Create the Socket-class and keep hold of it. Then create a new instance of SocketAsyncEventArgs when ever you need to do some work with the connection. And then you get rid of the instance.
It is possible to re-use the SocketAsyncEventArgs-class but I’ve found this to be cumbersome. It’s easier to start with a fresh instance every time. This means that you can attach a meaningful event handler for the Completed-event, like “OnConnectionCompleted” or “OnMessageReceived”. If you re-use the same instance every time, you must use a generic event handler like “OnOperationCompleted” where you must check for the LastOperation-property and route the handling to the correct method.
Creating the connection
Now that we understand the two central pieces of the Socket-namespace it is time to start out IRC-client project. To better understand the whole protocol one can study the available specs. But at this point it’s enough if you understand the basic characteristics of an IRC-connection:
- Client uses the TCP-protocol to connect to an IRC-server
- The connection must stay open as long as the client is used
- Messages are sent and received in plain text (except in the case of SSL connections)
To create a connection we must known the address of one of the IRC-servers. QuakeNet is one of the largest IRC networks out there and we will be connecting one of their servers during these tutorials. We will use the server address fi.quakenet.org. This server should allow connections from every part of the world but if it doesn’t you can use one of the servers listed in here.IRC-server are usually available in the port 6667, and this rule applies to fi.quakenet.org too.
During these tutorials we will create a class called IrcConnection which we will use as a wrapper around the System.Net.Sockets and the IRC-protocol. At this point we are only concerned of opening the connection to the IRC-server so here’s the code from which we will start moving forward:
public class IrcClient { public void CreateConnection(string serverAddress, int port) { } }
Like previously mentioned, an instance of the Socket-class is required to get things working. And also, we don’t want to lose our instance. So we create it and store it as a field inside the IrcClient. To create the instance we need to pass it few parameters:
- AddressFamily-enum: This enum specifies the addressing scheme for our Socket. Potential values include InterNetwork (IPv4), InterNetworkV6 (IPv6) and so on. In our case we will use the InterNetwork.
- SocketType-enum: From the MSDN-documentation, “The SocketType enumeration provides several options for defining the type of Socket that you intend to open.” When creating an IRC-client we will use the Stream-type.
- ProtocolType-enum: Last but not least you must make a selection between the TCP and UDP-protocols. In our case the selection is TCP.
With these selections we can create our instance of the Socket-class:
this.connection = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
As you probably noticed, you don’t pass the server’s address or port to the constructor. What makes things even more strange is that those parameters aren’t passed to the ConnectAsync-method either! This is where the SocketAsyncEventArgs-class comes into play. To create the connection, you must first instantiate a SocketAsyncEventArgs-object. You then tell it the server’s endpoint (address and port) and using this object, you can call the Socket’s ConnectAsync-method. When the ConnectAsync completes, your SocketAsyncEventArgs-instance will raise the Completed-event:
private Socket connection; private string server; private int serverPort; public void CreateConnection(string serverAddress, int port) { this.connection = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); this.server = serverAddress; this.serverPort = port; var connectionOperation = new SocketAsyncEventArgs { RemoteEndPoint = new DnsEndPoint(this.server, this.serverPort) }; connectionOperation.Completed += OnConnectionToServerCompleted; this.connection.ConnectAsync(connectionOperation); } private void OnConnectionToServerCompleted(object sender, SocketAsyncEventArgs e) { }
And that’s about it! If the provided server address and port were correct, your connection is now open and you can start sending and receiving message between your client and the server. The SocketError-property in SocketAsyncEventArgs can be used to check if the connection was opened successfully.
if (e.SocketError != SocketError.Success) { ... }
Source code
The whole source code for this tutorial is available from the GitHub.