C # Network Programming (basic concepts and operations) - Part.1

C # Network Programming (basic concepts and operations) - Part.1

introduction

C # Network Programming series of articles planned simply to teach the fundamentals of network programming, because of my limited skill in this regard, we can only provide some preliminary introductory knowledge, hoping to provide some help for a friend just beginning to learn. If you want more in-depth content, you can refer to books.

This article is the first article in the series, it focuses on the basic concepts of network programming based sockets (the Socket), wherein the development model includes three TCP protocol, sockets, chat programs, and two basic operations: listens port, remote connection server; the second describes a simple example:, the server receives the character string transmitted from the client to the server and the print string, the string to upper case, and then back to the string the client, the client returned last print string; Part III the second is an intensive, tells the second question is not resolved, and the use of asynchronous transfer mode to complete the second and equally functionality; Part IV shows how the send and receive files between the client and server; the fifth chapter implements a can chat online chat and file transfer program, in fact, is a comprehensive application of the previous knowledge.

Associated with this article there is an article: C # write a simple chat program , but this program is less than this series in chat chat program is powerful, implementation is not the same.

The basic concept of network programming

1. The connection-oriented transport protocols: TCP

For TCP I do not want to say too much, which is a university course, also involves computer science, and I am not "academic", for this part, I think as a developer, you only need to master the concepts related to program on it , do not need to study too abstruse.

First, we know that TCP is a connection-oriented , which means that two of the remote host (or called process, because in fact remote communication is communication between processes, and the process is running the program), you must first be a handshake process, make sure the connection is successful, before you can transfer the actual data. For example, A wants to process the string "It's a fine day today", to the process B, it must first establish a connection. In this process, it first needs to know the location of the process B (host address and port number). Then sends a request message does not contain the actual data, we can call this the message "hello". If the process B receives this "hello", he replies with a "hello" to process A, process A and then only send the actual data "It's a fine day today" .

About TCP second you need to know is that it is full-duplex 's. This means that if the process (such as process A, process B) on both hosts, good once the connection is established, then the data can flow both from the B A, may also flow from B A. In addition, it is the point , and means that a TCP connection is always, in the transmission, via a connection between the two data to the multiple receivers is not possible. TCP also has a feature that is known as a reliable data transmission side, which means the connection is established, send the data will be able to reach, and is ordered, that is made when you made ABC, then the income received it must also be ABC, BCA and will not be or something.

Programming The most important concept is associated with TCP sockets . We should know the network seven agreements, if we apply the above process, presentation layer, session layer in general terms counted as one (some textbooks is so divided), then we write web applications on the application layer, and we know that TCP is the transport layer protocol belong, how we use it in the service of the transport layer application layer (message or file upload and download)? We all know that in the application we use interfaces to achieve separation between the application layer and the transport layer, is the use of sockets to be separated. It is like the transport layer to the application layer to open a small port, the application sends data to the remote through this small opening, or receiving remote data sent; after and within this small opening, i.e. data enters the mouth, or the data from the port before it, we do not know or need to know, we do not care how it is transmitted, which is a network other levels of work.

For example, if you want to write e-mails sent to distant friends, so how do you write a letter, the letter pack, is an application layer, how to write letters, how completely packed's up to us; and when we dropped the letter into the mailbox, that mailbox is opening a socket, after entering the socket, the other is the level of work (post office, road or route traffic control, etc.) transport layer, the network layer and so on. We never do not care how the letter was sent to Beijing from Xi'an, we only know that written into the mailbox on OK. Can use the following two figures is represented by:

Note that in the above figures, the two hosts are on equal footing, but in accordance with the agreement, we will initiate the request of a party called the client, and the other end is called the server. We can see the dialogue between the two programs is accomplished through the socket of the entrance, in fact socket contains the most important information is the two: Connect to the local port information of the remote (local address and port number), is connected to the remote port information (remote address and port number). Note that the above words in a subtle change, is a local address, a remote address.

Here again there was a term port . We generally run on a computer with a lot of applications that may require interaction with the remote host, the remote host will need to have an ID to identify what it wants to deal with the application on the local machine, here is ID port. Data port is assigned to an application, it is always from this port for this application. There is such a good example: The host address can be thought of as a telephone number, and the port number thought of as an extension number.

In .NET, although we can direct socket programming, but .NET provides two classes socket programming will be a package, so that we can be more convenient to use, and these two classes is TcpClient TcpListener, its relationship with the socket as follows:

From the above it can be seen in FIG TcpListener TcpClient and a socket is encapsulated. Which may be seen, located TcpListener received stream, the output stream of the TcpClient located (practically TcpListener after receiving a request, the TcpClient is created, which itself is in continuous listening state, can receive data from the TcpClient complete. this figure a little less accurate, but I do not think of a better painting will become more apparent number) when you see the code behind.

We consider a case: two hosts, Hosts A and B, where at first they do not know who's who, when they want to engage in dialogue, always requires one party initiates a connection, while the other needs of the machine a port is listening. After the connection request is received while listening party, and a connection is established, when the transmit and receive data between them, the party initiating the connection does not need to listen. Because full-duplex connection, it can use the existing connection to transmit and receive data. And we have already done a definition: the party initiating the connection is called the client, another section called the server, you can now draw: always use the server in TcpListener class, because it needs to establish an initial connection .

2. The three modes of Internet chat program

实现一个网络聊天程序本应是最后一篇文章的内容,也是本系列最后的一个程序,来作为一个终结。但是我想后面更多的是编码,讲述的内容应该不会太多,所以还是把讲述的东西都放到这里吧。

当采用这种模式时,即是所谓的完全点对点模式,此时每台计算机本身也是服务器,因为它需要进行端口的侦听。实现这个模式的难点是:各个主机(或终端)之间如何知道其它主机的存在?此时通常的做法是当某一主机上线时,使用UDP协议进行一个广播(Broadcast),通过这种方式来“告知”其它主机自己已经在线并说明位置,收到广播的主机发回一个应答,此时主机便知道其他主机的存在。这种方式我个人并不喜欢,但在 C#编写简单的聊天程序 这篇文章中,我使用了这种模式,可惜的是我没有实现广播,所以还很不完善。

第二种方式较好的解决了上面的问题,它引入了服务器,由这个服务器来专门进行广播。服务器持续保持对端口的侦听状态,每当有主机上线时,首先连接至服务器,服务器收到连接后,将该主机的位置(地址和端口号)发往其他在线主机(绿色箭头标识)。这样其他主机便知道该主机已上线,并知道其所在位置,从而可以进行连接和对话。在服务器进行了广播之后,因为各个主机已经知道了其他主机的位置,因此主机之间的对话就不再通过服务器(黑色箭头表示),而是直接进行连接。因此,使用这种模式时,各个主机依然需要保持对端口的侦听。在某台主机离线时,与登录时的模式类似,服务器会收到通知,然后转告给其他的主机。

第三种模式是我觉得最简单也最实用的一种,主机的登录与离线与第二种模式相同。注意到每台主机在上线时首先就与服务器建立了连接,那么从主机A发往主机B发送消息,就可以通过这样一条路径,主机A --> 服务器 --> 主机B,通过这种方式,各个主机不需要在对端口进行侦听,而只需要服务器进行侦听就可以了,大大地简化了开发。

而对于一些较大的文件,比如说图片或者文件,如果想由主机A发往主机B,如果通过服务器进行传输效率会比较低,此时可以临时搭建一个主机A至主机B之间的连接,用于传输大文件。当文件传输结束之后再关闭连接(桔红色箭头标识)。

除此以外,由于消息都经过服务器,所以服务器还可以缓存主机间的对话,即是说当主机A发往主机B时,如果主机B已经离线,则服务器可以对消息进行缓存,当主机B下次连接到服务器时,服务器自动将缓存的消息发给主机B。

本系列文章最后采用的即是此种模式,不过没有实现过多复杂的功能。接下来我们的理论知识告一段落,开始下一阶段――漫长的编码。

基本操作

1.服务端对端口进行侦听

接下来我们开始编写一些实际的代码,第一步就是开启对本地机器上某一端口的侦听。首先创建一个控制台应用程序,将项目名称命名为ServerConsole,它代表我们的服务端。如果想要与外界进行通信,第一件要做的事情就是开启对端口的侦听,这就像为计算机打开了一个“门”,所有向这个“门”发送的请求(“敲门”)都会被系统接收到。在C#中可以通过下面几个步骤完成,首先使用本机Ip地址和端口号创建一个System.Net.Sockets.TcpListener类型的实例,然后在该实例上调用Start()方法,从而开启对指定端口的侦听。

using System.Net;               // 引入这两个命名空间,以下同
using System.Net.Sockets;
using ... // 略

class Server {
    static void Main(string[] args) {
        Console.WriteLine("Server is running ... ");
        IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
        TcpListener listener = new TcpListener(ip, 8500);

        listener.Start();           // 开始侦听
        Console.WriteLine("Start Listening ...");

        Console.WriteLine("\n\n输入\"Q\"键退出。");
        ConsoleKey key;
        do {
            key = Console.ReadKey(true).Key;
        } while (key != ConsoleKey.Q);
    }
}

// 获得IPAddress对象的另外几种常用方法:
IPAddress ip = IPAddress.Parse("127.0.0.1");
IPAddress ip = Dns.GetHostEntry("localhost").AddressList[0];   

上面的代码中,我们开启了对8500端口的侦听。在运行了上面的程序之后,然后打开“命令提示符”,输入“netstat-a”,可以看到计算机器中所有打开的端口的状态。可以从中找到8500端口,看到它的状态是LISTENING,这说明它已经开始了侦听:

  TCP    jimmy:1030             0.0.0.0:0              LISTENING
  TCP    jimmy:3603             0.0.0.0:0              LISTENING
  TCP    jimmy:8500             0.0.0.0:0              LISTENING
  TCP    jimmy:netbios-ssn     0.0.0.0:0              LISTENING

在打开了对端口的侦听以后,服务端必须通过某种方式进行阻塞(比如Console.ReadKey()),使得程序不能够因为运行结束而退出。否则就无法使用“netstat -a”看到端口的连接状态,因为程序已经退出,连接会自然中断,再运行“netstat -a”当然就不会显示端口了。所以程序最后按“Q”退出那段代码是必要的,下面的每段程序都会含有这个代码段,但为了节省空间,我都省略掉了。

2.客户端与服务端连接

2.1单一客户端与服务端连接

当服务器开始对端口侦听之后,便可以创建客户端与它建立连接。这一步是通过在客户端创建一个TcpClient的类型实例完成。每创建一个新的TcpClient便相当于创建了一个新的套接字Socket去与服务端通信,.Net会自动为这个套接字分配一个端口号,上面说过,TcpClient类不过是对Socket进行了一个包装。创建TcpClient类型实例时,可以在构造函数中指定远程服务器的地址和端口号。这样在创建的同时,就会向远程服务端发送一个连接请求(“握手”),一旦成功,则两者间的连接就建立起来了。也可以使用重载的无参数构造函数创建对象,然后再调用Connect()方法,在Connect()方法中传入远程服务器地址和端口号,来与服务器建立连接。

这里需要注意的是,不管是使用有参数的构造函数与服务器连接,或者是通过Connect()方法与服务器建立连接,都是同步方法(或者说是阻塞的,英文叫block)。它的意思是说,客户端在与服务端连接成功、从而方法返回,或者是服务端不存、从而抛出异常之前,是无法继续进行后继操作的。这里还有一个名为BeginConnect()的方法,用于实施异步的连接,这样程序不会被阻塞,可以立即执行后面的操作,这是因为可能由于网络拥塞等问题,连接需要较长时间才能完成。网络编程中有非常多的异步操作,凡事都是由简入难,关于异步操作,我们后面再讨论,现在只看同步操作。

创建一个新的控制台应用程序项目,命名为ClientConsole,它是我们的客户端,然后添加下面的代码,创建与服务器的连接:

class Client {
    static void Main(string[] args) {

        Console.WriteLine("Client Running ...");
        TcpClient client = new TcpClient();
        try {
            client.Connect("localhost", 8500);      // 与服务器连接
        } catch (Exception ex) {
            Console.WriteLine(ex.Message);
            return;
        }
        // 打印连接到的服务端信息
        Console.WriteLine("Server Connected!{0} --> {1}",
            client.Client.LocalEndPoint, client.Client.RemoteEndPoint);

        // 按Q退出
    }
}

上面带代码中,我们通过调用Connect()方法来与服务端连接。随后,我们打印了这个连接消息:本机的Ip地址和端口号,以及连接到的远程Ip地址和端口号。TcpClient的Client属性返回了一个Socket对象,它的LocalEndPoint和RemoteEndPoint属性分别包含了本地和远程的地址信息。先运行服务端,再运行这段代码。可以看到两边的输出情况如下:

// 服务端:
Server is running ...
Start Listening ...

// 客户端:
Client Running ...
Server Connected!127.0.0.1:4761 --> 127.0.0.1:8500

我们看到客户端使用的端口号为4761,上面已经说过,这个端口号是由.NET随机选取的,并不需要我们来设置,并且每次运行时,这个端口号都不同。再次打开“命令提示符”,输入“netstat -a”,可以看到下面的输出:

  TCP    jimmy:8500             0.0.0.0:0              LISTENING
  TCP    jimmy:8500             localhost:4761         ESTABLISHED
  TCP    jimmy:4761             localhost:8500         ESTABLISHED

从这里我们可以得出几个重要信息:1、端口8500和端口4761建立了连接,这个4761端口便是客户端用来与服务端进行通信的端口;2、8500端口在与客户端建立起一个连接后,仍然继续保持在监听状态。这也就是说一个端口可以与多个远程端口建立通信,这是显然的,大家众所周之的HTTP使用的默认端口为80,但是一个Web服务器要通过这个端口与多少个浏览器通信啊。

2.2多个客户端与服务端连接

那么既然一个服务器端口可以应对多个客户端连接,那么接下来我们就看一下,如何让多个客户端与服务端连接。如同我们上面所说的,一个TcpClient就是一个Socket,所以我们只要创建多个TcpClient,然后再调用Connect()方法就可以了:

class Client {
    static void Main(string[] args) {

        Console.WriteLine("Client Running ...");
        TcpClient client;

        for (int i = 0; i <= 2; i++) {
            try {
                client = new TcpClient();
                client.Connect("localhost", 8500);      // 与服务器连接
            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
                return;
            }

            // 打印连接到的服务端信息
            Console.WriteLine("Server Connected!{0} --> {1}",
                client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
        }                  

        // 按Q退出
    }
}

上面代码最重要的就是client = new TcpClient()这句,如果你将这个声明放到循环外面,再循环的第二趟就会发生异常,原因很显然:一个TcpClient对象对应一个Socket,一个Socket对应着一个端口,如果不使用new操作符重新创建对象,那么就相当于使用一个已经与服务端建立了连接的端口再次与远程建立连接

此时,如果在“命令提示符”运行“netstat -a”,则会看到类似下面的的输出:

  TCP    jimmy:8500             0.0.0.0:0               LISTENING
  TCP    jimmy:8500             localhost:10282        ESTABLISHED
  TCP    jimmy:8500             localhost:10283        ESTABLISHED
  TCP    jimmy:8500             localhost:10284        ESTABLISHED
  TCP    jimmy:10282            localhost:8500         ESTABLISHED
  TCP    jimmy:10283            localhost:8500         ESTABLISHED
  TCP    jimmy:10284            localhost:8500         ESTABLISHED

可以看到创建了三个连接对,并且8500端口持续保持侦听状态,从这里以及上面我们可以推断出TcpListener的Start()方法是一个异步方法。

3.服务端获取客户端连接

3.1获取单一客户端连接

上面服务端、客户端的代码已经建立起了连接,这通过使用“netstat -a”命令,从端口的状态可以看出来,但这是操作系统告诉我们的。那么我们现在需要知道的就是:服务端的程序如何知道已经与一个客户端建立起了连接?

服务器端开始侦听以后,可以在TcpListener实例上调用AcceptTcpClient()来获取与一个客户端的连接,它返回一个TcpClient类型实例。此时它所包装的是由服务端去往客户端的Socket,而我们在客户端创建的TcpClient则是由客户端去往服务端的。这个方法是一个同步方法(或者叫阻断方法,block method),意思就是说,当程序调用它以后,它会一直等待某个客户端连接,然后才会返回,否则就会一直等下去。这样的话,在调用它以后,除非得到一个客户端连接,不然不会执行接下来的代码。一个很好的类比就是Console.ReadLine()方法,它读取输入在控制台中的一行字符串,如果有输入,就继续执行下面代码;如果没有输入,就会一直等待下去。

class Server {
    static void Main(string[] args) {
        Console.WriteLine("Server is running ... ");
        IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
        TcpListener listener = new TcpListener(ip, 8500);

        listener.Start();           // 开始侦听
        Console.WriteLine("Start Listening ...");

        // 获取一个连接,中断方法
        TcpClient remoteClient = listener.AcceptTcpClient();

        // 打印连接到的客户端信息
        Console.WriteLine("Client Connected!{0} <-- {1}",
           remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);

        // 按Q退出
    }
}

运行这段代码,会发现服务端运行到listener.AcceptTcpClient()时便停止了,并不会执行下面的Console.WriteLine()方法。为了让它继续执行下去,必须有一个客户端连接到它,所以我们现在运行客户端,与它进行连接。简单起见,我们只在客户端开启一个端口与之连接:

class Client {
    static void Main(string[] args) {

        Console.WriteLine("Client Running ...");
        TcpClient client = new TcpClient();
        try {
            client.Connect("localhost", 8500);      // 与服务器连接
        } catch (Exception ex) {
            Console.WriteLine(ex.Message);
            return;
        }
        // 打印连接到的服务端信息
        Console.WriteLine("Server Connected!{0} --> {1}",
            client.Client.LocalEndPoint, client.Client.RemoteEndPoint);

        // 按Q退出
    }
}

此时,服务端、客户端的输出分别为:

// 服务端
Server is running ...
Start Listening ...
Client Connected!127.0.0.1:8500 <-- 127.0.0.1:5188

// 客户端
Client Running ...
Server Connected!127.0.0.1:5188 --> 127.0.0.1:8500

3.2获取多个客户端连接

现在我们再接着考虑,如果有多个客户端发动对服务器端的连接会怎么样,为了避免你将浏览器向上滚动,来查看上面的代码,我将它拷贝了下来,我们先看下客户端的关键代码:

TcpClient client;

for (int i = 0; i <=2; i++) {
    try {
        client = new TcpClient();
        client.Connect("localhost", 8500);      // 与服务器连接
    } catch (Exception ex) {
        Console.WriteLine(ex.Message);
        return;
    }

    // 打印连接到的服务端信息
    Console.WriteLine("Server Connected!{0} --> {1}",
        client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
}

如果服务端代码不变,我们先运行服务端,再运行客户端,那么接下来会看到这样的输出:

// 服务端
Server is running ...
Start Listening ...
Client Connected!127.0.0.1:8500 <-- 127.0.0.1:5226

// 客户端
Client Running ...
Server Connected!127.0.0.1:5226 --> 127.0.0.1:8500
Server Connected!127.0.0.1:5227 --> 127.0.0.1:8500
Server Connected!127.0.0.1:5228 --> 127.0.0.1:8500

就又回到了本章第2.2小节“多个客户端与服务端连接”中的处境:尽管有三个客户端连接到了服务端,但是服务端程序只接收到了一个。这是因为服务端只调用了一次listener.AcceptTcpClient(),而它只对应一个连往客户端的Socket。但是操作系统是知道连接已经建立了的,只是我们程序中没有处理到,所以我们当我们输入“netstat -a”时,仍然会看到3对连接都已经建立成功。

为了能够接收到三个客户端的连接,我们只要对服务端稍稍进行一下修改,将AcceptTcpClient方法放入一个do/while循环中就可以了:

Console.WriteLine("Start Listening ...");

while (true) {
    // 获取一个连接,同步方法
    TcpClient remoteClient = listener.AcceptTcpClient();
    // 打印连接到的客户端信息
    Console.WriteLine("Client Connected!{0} <-- {1}",
        remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);
}

这样看上去是一个死循环,但是并不会让你的机器系统资源迅速耗尽。因为前面已经说过了,AcceptTcpClient()再没有收到客户端的连接之前,是不会继续执行的,它的大部分时间都在等待。另外,服务端几乎总是要保持在运行状态,所以这样做并无不可,还可以省去“按Q退出”那段代码。此时再运行代码,会看到服务端可以收到3个客户端的连接了。

Server is running ...
Start Listening ...
Client Connected!127.0.0.1:8500 <-- 127.0.0.1:5305
Client Connected!127.0.0.1:8500 <-- 127.0.0.1:5306
Client Connected!127.0.0.1:8500 <-- 127.0.0.1:5307

本篇文章到此就结束了,接下来一篇我们来看看如何在服务端与客户端之间收发数据。

转载于:https://www.cnblogs.com/JimmyZhang/archive/2008/09/07/1286300.html

Guess you like

Origin blog.csdn.net/weixin_33939380/article/details/93444046