第十五章《网络编程》第3节:基于TCP协议的网络编程

时间:2022-12-17 07:56:02


如果希望开发一个类似于QQ那样的即时通信软件,就必须使用基于TCP协议的编程技术。基于TCP协议的编程可以实现在通信两端建立虚拟网络链路,这样的话通信两端的程序就能通过虚拟网络链路来进行通信。Java语言提供了专门基于TCP协议的类,使用这些类就能够以流的方式完成两个端点之间的通信。

15.3.1 TCP协议简介

在15.1小节中曾经介绍过IP协议,IP协议只能保证计算机在发送和接收数据时有明确的地址,但并不能保证数据一定会成功的发送到目标地址,也不能保证数据在发送和接收过程中不出现数据包顺序不发生错乱的情况。如果希望数据能够被正确的接收并整合,还需要计算机上安装TCP协议。

TCP的全称是“Transmission Control Protocol”,翻译成中文意为“传输控制协议”。当两台计算机建立连接时,TCP协议会让它们建立一个连接,这个连接用于发送和接收数据。当一台计算机发送数据时,TCP协议会把这些数据按照一定的规则整理好并发送出去,而另一台计算机在接收到这些数据时会将数据按规则进行还原,因此,TCP协议保证了数据包在传输过程中准确无误。

TCP协议具有一个很重要的机制,那就是重发机制。当一个通信实体发送消息给另一个通信实体后,需要收到对方的确认,如果没有收到另一个通信实体的确认,则会重新发送一次信息。

通过重发机制,TCP协议向应用程序提供了可靠的通信连接,使它能够适应网上的各种变化。即使在网络出现暂时堵塞的情况下,TCP协议也能保证通信的可靠性。

虽然IP和TCP这两个协议的功能不尽相同,也可以分开单独使用,但它们是在同一时期作为一个协议来设计的,并且在功能上也是互补的。只有两者结合起来,才能保证Internet在复杂的环境下正常运行。凡是要连接到Internet的计算机,都必须同时安装和使用这两个协议,因此在实际中常把这两个协议统称为TCP/IP协议。

15.3.2创建TCP服务器端

读者想要学习TCP通信程序的开发,必须先理解“通信实体”和“通信端点”这两个概念。所谓通信实体就是通信设备,例如个人电脑、手机等都是通信实体。而通信端点则是运行在通信实体上的用于发送和接收信息的对象。在Java语言中,使用Socket类来表示通信端点,每个通信端点都是使用流来实现信息的接收和发送。

TCP协议下的通信并没有服务器和客户端之分,但为了保证通信能够顺利完成,需要有一个通信实体先做好准备。通信实体怎样做好准备呢?它需要先创建一个服务器对象,这个服务器在创建好之后就可以进入等待状态,等着另一个通信端点来与它建立连接。当这个服务器对象与一个主动联系它的通信端点成功的建立连接后,服务器对象自身并不会与通信端点直接进行通信,而是创建一个Socket对象与另一个通信端点互发消息,当然另一个通信端点也是Socket类的对象。

Java语言中,以ServerSocket类来表示TCP协议下的服务器。ServerSocket有三个常用的构造方法,如表15-4所示。

表15-4 ServerSocket的构造方法

构造方法

功能

ServerSocket(int port)

用port所指定的端口号创建一个ServerSocket

ServerSocket(int port,int backlog)

创建ServerSocket,以port作为端口号,以backlog作为队列长度

ServerSocket(int port,int backlog,InetAddress address)

在多台计算机组成的集群中,以address指定计算机的IP地址,所创建的ServerSocket就位于指定IP地址的计算机

当创建了一个ServerSocket类对象后,调用其accept()方法就能让服务器进入等待状态。当一个通信端点与服务器建立连接后,服务器会结束等待并创建一个Socket对象作为通信端点来与另一个通信端点进行交互。当使用服务器结束,应该调用ServerSocket的close()方法关闭服务器。

15.3.3使用Socket完成通信

ServerSocket并不具备通信功能,真正完成通信的是Socket类的对象,因此一个Socket类对象就是一个通信端点,通信端点也叫套接字,每一个通信端点都具有发送和接收消息的功能。Socket类的常用构造方法如表15-5所示。

表15-5 Socket的构造方法

构造方法

功能

Socket(String host, int port)

创建一个流套接字,并将其与指定的主机上的指定端口号连接起来。

public Socket(InetAddress host, int port)

创建一个套接字,并将其与指定的IP地址中的指定端口号连接起来

Socket(String remoteAddress, int port, InetAddress localAddr, int localPort)

指定远程主机、远程端口的Socket,并指定本地IP地址和本地端口,适用于本地主机有多个IP地址的情形。

Socket(InetAddress remoteAddress, int port, InetAddress localAddr, int localPort)

指定远程主机、远程端口的Socket,并指定本地IP地址和本地端口,适用于本地主机有多个IP地址的情形。

从表15-5可以看出:每次创建Socket对象时,都会指定要连接的主机的IP地址和端口号。此处必须要强调:创建Socket对象时,对应主机上的ServerSocket必须已经调用accept()方法做好被连接的准备,因为一旦一个Socket对象被创建,它就会立即主动连接ServerSocket,此时如果ServerSocket没有做好准备就会抛出异常。

当Socket对象与ServerSocket建立连接后,服务器会结束等待并由accept()方法创建一个Socket对象作为通信端点来与当前Socket对象进行交互。相互通信的两个Socket对象都会通过流进行信息的收发,Socket获得流对象的方法如表15-6所示。

表15-6 Socket获得流的方法

方法

功能

InputStream getInputStream()


返回该Socket对象对应的输入流,这个输入流用以读取对方Socket发送的消息。

OutputStream getOutputStream()

返回该Socket对象对应的输出流,这个输出流用于向对方Socket发送数据

下面的【例15_04】展示了如何使用Socket实现及时通信。

【例15_04 使用Socket完成通信】

Exam15_04.java

import java.io.*;
import java.net.*;
class Server1 extends Thread{
public void run() {
try {
ServerSocket server = new ServerSocket(5678);
//等待对方发起连接,连接建立后产生一个Socket
Socket socket = server.accept();
PrintWriter pw = new PrintWriter(socket.getOutputStream());
InputStreamReader isr = new InputStreamReader(socket.getInputStream());
BufferedReader br = new BufferedReader(isr);
pw.println("服务器发出消息!");//向对方发送消息
pw.println("end");//通知对方消息结束
pw.flush();
String message;
while ((message=br.readLine())!=null){
if ("end".equals(message)){
break;
}
System.out.println(message);//打印对方发送的消息
}
pw.close();
isr.close();
br.close();
socket.close();
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

class Client1 extends Thread{
public void run() {
try {
//客户端线程先睡眠1000毫秒以保证服务器线程已创建好ServerSocket
Thread.sleep(1000);
Socket socket = new Socket(InetAddress.getLocalHost(),5678);
PrintWriter pw = new PrintWriter(socket.getOutputStream());
InputStreamReader isr = new InputStreamReader(socket.getInputStream());
BufferedReader br = new BufferedReader(isr);
pw.println("客户端发出消息!");//向对方发送消息
pw.println("end");//通知对方消息结束
pw.flush();
String message;
while ((message=br.readLine())!=null){
if ("end".equals(message)){
break;
}
System.out.println(message);//打印对方发送的消息
}
pw.close();
isr.close();
br.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public class Exam15_04 {
public static void main(String[] args) {
new Server1().start();//创建并启动服务器线程
new Client1().start();//创建并启动客户端线程
}
}

【例15_04】中有两个线程Server1和Client1,它们分别代表服务器和客户端。Server1启动后会创建ServerSocket对象,并调用accept()方法等候客户端对其进行连接。而Client1启动后会创建一个Socket对象去连接ServerSocket,一旦连接建立后,ServerSocket的accept()方法也会创建一个Socket与客户端的Socket相互收发消息。双方收发消息结束后,要关闭Socket和ServerSocket。【例15_04】的运行结果如图15-5所示。

第十五章《网络编程》第3节:基于TCP协议的网络编程

图15-5【例15_04】运行结果

15.3.4使用多线程完成通信

【例15_04】中,Server1线程和Client1线程直接使用Socket进行对话,双方发送消息结束后就关闭了ServerSocket,如果再有客户端希望连接ServerSocket并展开对话是无法完成的,因此此时ServerSocket已经被关闭。如何能让ServerSocket能够接受多个客户端的连接,并同时与它们对话呢?实际上使用多线程技术就能很好的解决这个问题。每当ServerSocket收到一个连接后,就会创建出Socket对象,如果每次产生Socket之后都由一个新的线程操作它来与客户端进行对话,而ServerSocket再次调用accept()方法等待下一个客户端与之连接,这样就能实现一个ServerSocket同时服务多个客户端,并且这种服务是对多个客户端同时进行的,不需要客户端进行排队。

按照以上思路,可以对【例15_04】进行改写。改写的程序中有Server2线程和Client2两个线程,但这两个线程并不直接进行对话。Server2负责创建一个ServerSocket,并在无限循环中调用accept()方法,这样ServerSocket就会一直等待客户端的连接。每当收到一个客户端连接后,Server2并不直接与之对话,而是创建一个ServerThread线程,让线程用产生Socket对象与客户端进行对话。而Client2也不负责对话,它只是不断的产生用于对话的Socket对象,并且把这个对象交给ClientThread线程,由ClientThread线程使用Socket对象完成对话的操作。改写后的程序就是下面的【例15_05】

【例15_05 多线程完成通信】

Exam15_05.java

import java.io.*;
import java.net.*;
class ServerThread extends Thread{
Socket socket;//线程要操作的Socket
ServerThread(Socket socket){
this.socket = socket;
}
public void run() {
try{
InputStreamReader isr = new InputStreamReader(socket.getInputStream());
BufferedReader br = new BufferedReader(isr);
String message;//Socket读取到的消息
while ((message=br.readLine())!=null){
if ("end".equals(message)){
break;
}
System.out.println(message);//打印所接收到的消息
}
isr.close();
br.close();
socket.close();
}catch (Exception e){
e.printStackTrace();
}
}
}

class Server2 extends Thread{
public void run() {
try {
ServerSocket server = new ServerSocket(6789);
while (true){//不断等待客户端连接
Socket socket = server.accept();//创建一个Socket对象
//产生一个线程,并赋予线程一个Socket对象
new ServerThread(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

class ClientThread extends Thread{
Socket socket;//线程要操作的Socket
String message;//线程要发送的消息
ClientThread(String name,Socket socket ,String message){
super(name);
this.socket = socket;
this.message = message;
}
public void run() {
try {
PrintWriter pw = new PrintWriter(socket.getOutputStream());
pw.println(getName()+"发送的消息是:"+message);
pw.println("end");
pw.flush();
pw.close();
socket.close();
}catch (Exception e){
e.printStackTrace();
}
}
}

class Client2 extends Thread{
public void run() {
try {
//先睡眠1000毫秒以保证服务器线程已创建好ServerSocket
Thread.sleep(1000);
for (int i=1;i<=5;i++){
Socket socket = new Socket(InetAddress.getLocalHost(),6789);
new ClientThread(i+"号线程",socket,10000+i+"").start();
}
}catch (Exception e){
e.printStackTrace();
}
}
}

public class Exam15_05 {
public static void main(String[] args) {
new Server2().start();
new Client2().start();
}
}

【例15_05】的运行结果如图15-6所示。

第十五章《网络编程》第3节:基于TCP协议的网络编程

图15-6【例15_05】运行结果

从图15-6可以看出:一个ServerSocket先后接受了多个客户端的连接并收到了它们发送的消息,但是并不是先产生的ClientThread线程就会先得到服务器的回应。无论得到回应的顺序如何,每一个ClientThread都成功的用Socket把信息发送给了服务器。由于accept()方法是在一个无限循环中不断等待新的连接,因此【例15_05】在产生运行结果后程序并不会结束,读者可以单击“Stop”按钮强行终止程序。