50、Java基础教程之网络编程

  • *本节学习目标
  • 1️⃣ 网络编程的概念
      • * TCP和 UDP协议
  • 2️⃣ Socket 与ServerSocket 类
  • 3️⃣ 网络编程实战——Echo 程序
  • 4️⃣ 应用场景
  • * 总结

*

*本节学习目标

  • 了解多线程与网络编程的操作关系;
  • 了解网络程序开发的主要模式;
  • 了解 TCP 程序的基本实现;

1️⃣ 网络编程的概念

在Java中,网络编程的核心意义是实现不同电脑主机之间的数据交互。Java采用了一种简化的概念,将这个过程进一步抽象为JVM(Java虚拟机)进程之间的通信。可以在同一台电脑上同时运行多个JVM进程,而这些不同的JVM进程能够相互通信,它们在网络编程中被视为不同的主机。

*

图1远程访问——不同的JVM进程间的访问

每个JVM进程都有自己的内存空间和资源,并且可以在不同的物理机或同一台物理机上运行。通过使用Java提供的网络编程API,我们可以在这些JVM进程之间建立连接、发送和接收数据。

这种以JVM进程划分网络的方式带来了一些优势。首先,与传统的网络编程相比,它提供了更高层次的抽象,使得开发人员可以更方便地处理网络通信。其次,由于JVM进程可以在同一台物理机上运行,它们之间的通信速度更快,并且可以共享某些资源,从而提高了效率。

此外,我们前面介绍过的多线程篇中,也提供了一些并发编程的机制,如线程和锁,使得多个JVM进程之间的数据交互更加安全可靠。

而不同JVM进程间,彼此的数据访问也属于远程访问。在Java中存在的远程方法调用(Remote Method Invocation,RMI) 技术或企业 JavaBean(Enterprise JavaBean, EJB) 也都是依靠此概念进行使用的。

网络编程的实质意义在于数据的交互,而在交互过程中一定就会分为服务器端与客户端,而这两端的开发就会存在以下两种模式。

  • C/S 结构 (Client / Server), 此类模式的开发一般要编写两套程序,一套是客户端代码,另外一套属于服务器端代码。由于需要有编写程序,所以对于开发以及维护的成本较高。但是由于其使用的是自己的连接端口与交换协议,所以安全性比较高。而 C/S 结构程序的开发分为两种:

  • TCP (传输控制协议,可靠的传输);

  • UDP (数据报协议)。

  • B/S 结构 (Browser /Server), 不再单独开发客户端代码,只开发一套服务器端程序,客户端将利用浏览器进行访问,这种模式只需要开发一套程序,但是安全性不高,因为使用的是公共的 HTTP 协议以及公共的80端口。

ASPPHPJSP 等都属于 B/S 的常见开发技术,这些都需要单独的服务器支持。而这些 B/S 技术要想实现互相访问,则需要Web Service技术支持。

* TCP和 UDP协议

  1. TCP(传输控制协议)
  • TCP提供了可靠的、面向连接的通信。它通过建立一个持久的连接,在发送数据之前,会先进行握手过程来确保双方的通信正常。这种可靠性是通过使用确认机制和重传机制来实现的,因此在传输过程中,既可以保证数据的顺序不变,也可以确保数据不被丢失或损坏。
  • TCP适合于需要确保数据完整、按照顺序到达的应用场景,例如文件传输、HTTP请求等。
  1. UDP(用户数据报协议)
  • UDP是一种无连接的协议,它不需要先建立连接就能直接发送数据。这意味着它没有像TCP那样的握手和断开连接的开销,具有较低的延迟和网络负载。
  • UDP提供了一种快速而简单的数据传输方式,但不保证数据的可靠性和顺序性。由于缺乏确认机制和重传机制,并且数据包可能在传输过程中丢失或乱序,所以需要应用层来处理可靠性和有序性的问题。
  • UDP适用于那些对实时性要求较高,但对数据完整性和顺序性要求相对较低的应用场景,例如音视频传输、实时游戏等。

在网络编程中,开发人员可以根据具体的业务需求选择使用TCPUDP。如果需要确保可靠的数据传输和有序性,则选择TCP;如果追求更低的延迟并能容忍一些数据丢失或乱序的情况,则可以选择UDP。在某些情况下,也可以同时使用两种协议来处理不同类型的数据。

2️⃣ Socket 与ServerSocket 类

java.net 包提供了网络编程有关的开发工具类,在此包中有以下两个主要的核心操作类。

  • ServerSocket 类:是一个封装支持 TCP 协议的操作类,主要工作在服务器端,用于接收客户端请求;
  • Socket 类:也是一个封装了 TCP 协议的操作类,每一个Socket 对象都表示一个客户端。

下面列出了ServerSocket 类的常用操作方法:

方法名称 类型 描述
public ServerSocket(int port) throws IOException 构造 开辟一个指定的端口监听, 一般使用 5000以上的端口
public Socket accept() throws IOException 普通 服务器端接收客户端请求,通过Socket 返回
public void close() throws IOException 普通 关闭服务器端

下面列出了Socket 类的常用操作方法:

方法名称 类型 描述
public Socket(String host, int port) throws UnknownHostException, IOException 构造 指定要连接的主机(IP地址)和端口
public OutputStream getOutputStream() throws IOException 普通 取得指定客户端的输出对象,使用 PrintStream操作
public InputStream getInputStream() throws IOException 普通 从指定的客户端读取数据,使用 Scanner操作

在客户端,程序可以通过 Socket 类的 getInputStream()方法,取得服务器的输出信息,在服务器端可以通过 getOutputStream() 方法取得客户端的输出信息,如下所示。

*

图2客户端与服务器端交互

在进行网络程序的开发中,最为重要的就是服务器端的功能。下边范例操作定义的服务器端将针对连接的客户端发出一个 “Hello World” 的字符串信息。

//	范例 1: 定义服务器端——主要使用 ServerSocket
package com.xiaoshan.demo;

import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;

public class HelloServer {
   
     
	public static void main(String[] args) throws Exception {
   
     
		ServerSocket server = new ServerSocket(9999);    //所有的服务器必须有端口
		System.out.println("等待客户端连接…");            //提示信息
		Socket client = server.accept();                                       //等待客户端连接
		//OutputStream并不方便进行内容的输出,所以利用打印流完成输出
		PrintStream out = new PrintStream(client.getOutputStream());
		out.println("Hello World !");                                               //输出数据
		out.close();
		client.close();
		server.close();
	}
}

程序执行结果:

等待客户端连接…

从程序执行结果可以看到,程序将出现阻塞情况, 一直到客户端连接后才会继续执行。

此程序在本机的9999端口上设置了一个服务器的监听操作,accept()方法表示打开服务器监听,这样当有客户端通过 TCP 连接方式连接到服务器端后,服务器端将利用 PrintStream 输出数据,当数据输出完毕后该服务器端就将关闭,所以本次定义的服务器只能处理一次客户端的请求。

//	范例 2: 编写客户端——Socket
package com.xiaoshan.demo;

import java.net.Socket;
import java.util.Scanner;

public class HelloClient {
   
     
	public static void main(String[] args) throws Exception {
   
     
		Socket client = new Socket("localhost", 9999);                       //连接服务器端
		//取得客户端的输入数据流对象,表示接收服务器端的输出信息
		Scanner scan = new Scanner(client.getInputStream());		//接收服务器端回应数据
		scan.useDelimiter("\n");	//设置分隔符
		if (scan.hasNext()){
   
     	//是否有数据
			System.out.println("【回应数据】"+ scan.next());	//取出数据
		}
		scan.close();
		client.close();
	}
}

程序执行结果:

【回应数据】Hello World !

在TCP 程序中,每一个 Socket 对象都表示一个客户端的信息,所以客户端程序要连接也必须依靠 Socket 对象操作。在实例化 Socket 类对象时必须设置要连接的主机名称(本机为 localhost,或者填写 IP 地址)以及连接端口号,当连接成功后就可以利用 Scanner 进行输入流数据的读取,这样就可以接收服务器端的回应信息。

3️⃣ 网络编程实战——Echo 程序

在网络编程中 Echo 是一个经典的程序开发模型,程序实现的功能:客户端随意输入信息并且将信息发送给服务器端,服务器端接收后前面加上一 个 “ECHO:” 的前缀标记后将数据返还给客户端。在本程序中服务器端既要接收客户端发送来的数据,又要向客户端输出数据,同时考虑到需要进行多次数据交换,所以每次连接后不应该立刻关闭服务器,而当用户输入了一些特定字符串 (例如:“byebye”) 后才表示可以结束本次的 Echo 操作。

//	范例 3: 实现服务器端
package	com.xiaoshan.demo;

import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class EchoServer {
   
     
	public static void main(String[] args) throws Exception {
   
     
		ServerSocket server = new ServerSocket(9999);		// 定义连接端口
		Socket client = server.accept();             	//等待客户端连接
		//得到客户端输入数据以及向客户端输出数据的对象,利用扫描流接收,打印流输出
		Scanner scan = new Scanner(client.getInputStream());
		PrintStream out = new PrintStream(client.getOutputStream());
		boolean flag = true;                          	//设置循环标记
		while(flag){
   
     
			if (scan.hasNext()){
   
     				//是否有内容输入
				String str = scan.next().trim();	//得到客户端发送的内容,并删除空格
				if(str.equalslgnoreCase("byebye")){
   
      	//程序结束标记
					out.println("拜拜,再见!"); 		//输出结束信息
					flag = false;					//退出循环
				}else{
   
     
					out.println("ECHO:" + str);		//回应输入信息,加“ECHO:” 前缀返回
				}
			}
		}
		scan.close();
		out.close();
		client.close();
		server.close();
	}
}

由于服务器端需要接收以及回应客户端的请求,所以在程序开始就首先取得了客户端的输入流与输出流,同时为了方便数据的读取与输出,分别使用了 ScannerPrintStream 进行 IO 的操作包装。考虑到该服务器端需要与客户端进行重复的数据交互,所以使用了一个 while 循环来不断实现数据的接收与输出。

//	范例 4: 定义客户端
package com.xiaoshan.demo;

import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

public class EchoClient {
   
     
	public static void main(String[] args) throws Exception{
   
     
		Socket client = new Socket("localhost", 9999);       //服务器地址与端口
		Scanner input = new Scanner(System.in);                         //键盘输入数据
		//利用Scanner包装客户端输入数据(服务器端输出),PrintStream包装客户端输出数据
		Scanner scan = new Scanner(client.getInputStream());
		PrintStream out = new PrintStream(client.getOutputStream());
		input.useDelimiter("\n");	//设置键盘输入分隔符
		scan.useDelimiter("\n");	//设置回应数据分隔符
		boolean flag = true;		//循环标志
		while (flag){
   
     
			System.out.print("请输入要发送数据:");
			if(input.hasNext()){
   
     		//键盘是否输入数据
				String str = input.next().trim();	//取得键盘输入数据
				out.println(str);		//发送数据到服务器端
				if(str.equalsIgnoreCase("byebye"){
   
      	//结束标记
					flag = false;		//结束循环
				}
				if (scan.hasNext()){
   
     		//服务器端有回应 
					System.out println(scan.next());	//输出回应数据
				}
			}
		}
		input.close();
		scan.close();
		out.close();
		client.close();
	}
}

程序实现了键盘数据的输入与发送操作,每当用户输入完信息后会将该信息发送到服务器端,只要发送的数据不是 “byebye” ,服务器端都会将发送的数据处理后再发送回客户端。由于需要重复输入, 所以在客户端上也使用了一个 while 循环进行控制。

范例4就实现了一个最简单的服务器端与客户端通讯,但是该程序只能连接一个客户端,不能连接其他客户端,因为所有的操作都是在主线程上进行的开发,也就是说该程序属于单线程的网络应用。而在实际的开发中一个服务器需要同时处理多个客户端的请求操作,在这样的情况下就可以利用多线程来进行操作,把每一个连接到服务器端的客户都作为一个独立的线程对象保留,如下所示。

*

图3多线程优化网络编程

//	范例 5:修改服务器端
package com.xiaoshan.demo;

import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

class EchoThread implements Runnable {
   
      	//建立线程类
	private Socket client;		//每个线程处理一个客户端
	
	public EchoThread(Socket client){
   
     	//创建线程对象时传递 Socket
		this.client = client;
	}
	
	@Override
	public void run(){
   
     
		try {
   
     			//每个线程对象取得各自Socket的输入流与输出流
			Scanner scan = new Scanner(client.getInputStream());
			PrintStream out = new PrintStream(client.getOutputStream());
			boolean flag = true;
			while(flag){
   
     		//控制多次接收操作
				if(scan.hasNext()){
   
     		//是否有内容
					String str = scan.next().trim();	//得到客户端发送的内容
					if (str.equalsIgnoreCase("byebye")){
   
     	// 程序结束
						out.println("拜拜,再见!");
						flag = false;				//退出循环
					}else{
   
     
						out.println("ECHO:"+str);	//回应信息
					}
				}
			}
			scan.close();
			out.close();
			client.close();
		} catch(Exception e){
   
     
			e.printStackTrace();
		}
	}
}

public class EchoServer {
   
     
	public static void main(String[] args) throws Exception{
   
     
		ServerSocket server = new ServerSocket(9999);	//在9999端口上监听
		boolean flag = true;                        	//循环标记
		while(flag){
   
                                	//接收多个客户端请求
			Socket client = server.accept();          	//客户端连接
			new Thread(new EchoThread(client)).start();	//创建并启动新线程
		}
		server.close();
	}
}

程序使用了多线程的概念来处理每一个客户端的请求,这样服务器就可以同时处理多个客户端的连接操作。当有新的客户端连接到服务器端后,会启动一个新的线程,这样在此线程中就会各自处理每 一个客户端的输入与输出操作。

4️⃣ 应用场景

Java网络编程具有广泛的应用场景,包括但不限于以下几个方面:

1、 客户端-服务器通信:Java网络编程可以用于构建基于C/S架构的分布式系统通过建立连接、传输数据和响应请求,可以实现客户端与服务器之间的通信,例如网站服务器与浏览器之间的HTTP通信、即时通讯程序等;
2、 文件传输和共享:Java网络编程可以用于文件传输和共享通过建立TCP连接,可以在客户端和服务器之间传输文件,如FTP(文件传输协议)或SFTP(SSH文件传输协议);
3、 远程过程调用(RPC):Java网络编程可以支持远程过程调用,使得应用程序可以在不同的主机上相互调用函数或方法这种方式可以实现分布式计算和服务架构,常见的例子是使用JavaRMI(远程方法调用)、ApacheThrift、gRPC等;
4、 Socket编程:Java网络编程中的SocketAPI允许开发者直接控制网络连接和数据传输可以创建基于TCP或UDP的Socket,实现点对点的通信,例如实时游戏、聊天应用等;
5、 网络爬虫和数据采集:Java网络编程提供了强大的能力来进行网页抓取和数据采集通过使用网络库和相关API,可以模拟浏览器行为,发送HTTP请求、解析HTML、提取数据并进行存储和分析;

总之,Java网络编程广泛应用于各种场景,包括网络服务端开发、网页爬取、远程过程调用、文件传输和Socket通信等。通过利用Java提供的网络API和库,开发人员可以构建高效、可靠的网络应用程序和分布式系统。

* 总结

本文介绍了网络编程的概念以及与之相关的TCPUDP协议。我们探讨了Java中的 SocketServerSocket类,这些重要的API用于实现网络通信。通过一个简单的实战示例——Echo程序,我们展示了如何使用Java进行网络编程,并解释了其工作原理。

网络编程在现代应用开发中扮演着至关重要的角色。它允许不同主机之间的数据交互,使得分布式系统成为可能。同时,网络编程也适用于各种场景,包括客户端-服务器通信、网络爬虫和数据采集、文件传输和共享、远程过程调用等。

无论是构建一个Web应用程序还是设计一个分布式系统,掌握网络编程技能都是至关重要的。Java提供了强大而丰富的网络编程库和API,支持多种协议和通信方式。通过理解并灵活运用这些概念和工具,开发人员可以轻松构建高性能、可靠的网络应用程序,并满足不同应用场景的需求。


[* ]nbsp_nbsp 4