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