目录
在Java中实现客户端与服务器端之间的连接通常可以通过Socket和ServerSocket类来实现。
那什么是Socket?
- Socket(套接字)是在网络编程中使用的一种抽象概念,用于建立不同计算机之间的通信连接。Socket允许不同计算机上的应用程序通过网络进行数据交换。
- 在Java编程中,Socket类是对套接字的抽象表示,提供了用于网络通信的接口。通过Socket类,可以实现客户端和服务器之间的通信,包括数据的发送和接收。
- 在一个通信过程中,客户端和服务器端各自创建一个Socket对象,并通过这个Socket对象来进行通信。客户端通过Socket连接服务器端的地址和端口号,然后可以通过Socket对象发送数据到服务器端;服务器端接收到客户端的请求后,也会创建一个新的Socket对象与客户端建立连接,从而进行数据交换。
- Socket对象在使用完毕后需要进行关闭操作,以释放资源和终止通信连接。在Java编程中,通常会在try-with-resources语句中使用Socket对象,以确保在通信结束时Socket对象能够被正确关闭。
- 总的来说,Socket类在网络编程中扮演着重要的角色,它提供了一种简单而有效的方式来实现计算机之间的通信和数据交换。
什么是ServerSocket?
- ServerSocket是Java编程语言中的一个类,用于在服务器端创建一个服务器套接字,监听客户端的连接请求。通过ServerSocket,服务器端可以接收来自客户端的连接,并与客户端建立通信连接。
- ServerSocket类提供了创建服务器套接字、监听端口、接受客户端连接请求等功能。通过ServerSocket的accept()方法,服务器端可以等待客户端的连接请求,并一旦有客户端请求连接,accept()方法将返回一个Socket对象,服务器端可以通过这个Socket对象与客户端进行通信。
- 在一个典型的网络应用程序中,服务器端通常会使用ServerSocket来初始化并监听一个特定的端口,等待客户端连接,然后处理客户端请求并进行相应的回复。ServerSocket类被广泛应用于Java网络编程中,用于实现服务器端的监听与响应。
- 需要注意的是,ServerSocket通常用在服务器端,用于监听客户端连接请求;而连接到服务器的客户端将会使用普通的Socket对象进行通信。通过ServerSocket和Socket相互配合,可以实现服务器端与多个客户端之间的连接通信。
- 总的来说,ServerSocket是Java中用于创建服务器套接字、接收客户端连接请求的重要类,是实现基于TCP协议的服务器端网络通信的关键组件。
代码展示:
import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.HashMap; //【服务端】 public class AIzzServer { public static HashMap<String,String> map=new HashMap<>(); static { map.put("你好", "你好呀,孙子"); map.put("hi", "hello,孙子"); map.put("hello", "hi,孙子"); map.put("吃了吗", "没呢,孙子"); map.put("很高兴认识你", "我也是哦"); } public static void main(String[] args) { try(ServerSocket serverSocket=new ServerSocket(8848)){ while(true) { Socket clientSocket=serverSocket.accept(); String clientIp=clientSocket.getInetAddress().getHostAddress(); //输入流:读取客户端发送的”问题“ //输出流:发送问题的答案给客户端 try(BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); BufferedWriter writer=new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))){ //读取来自客户端的”问题“ String question=reader.readLine(); if(question==null||question.length()==0){ continue; } System.out.printf("来自客户端[%s]的问题:%s\n",clientIp,question); String answer=map.get(question); answer=answer==null?"对不起,我不知道你在说什么!":answer; //将问题的答案输出至”客户端“ writer.write(answer); } } } catch (IOException e) { throw new RuntimeException(e); } } }
import java.io.*; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.util.Scanner; //【客户端】 public class AIzzClient { public static void main(String[] args) { try(Scanner input=new Scanner(System.in)){ //读取控制台输入的问题 String question=input.nextLine(); //创建Socket,输出流,输入流 try(Socket clientSocket=new Socket(InetAddress.getLocalHost(),8848); BufferedReader reader=new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); BufferedWriter writer=new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));){ //向服务端发送”问题“(输出至服务端) writer.write(question); writer.flush(); //暂时关闭输出流 clientSocket.shutdownOutput(); //接收服务端返回的“答案” String ans=reader.readLine(); System.out.println("来自服务端的回答:"+ans); } catch (UnknownHostException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } } }
代码解析:
上述代码展示了一个基于 TCP 协议的简单人工对打程序,包括服务端 `AIzzServer` 和客户端 `AIzzClient` 两个部分。
服务端 `AIzzServer` 主要负责:
- 1. 创建一个监听端口为 `8848` 的 `ServerSocket` ,等待客户端连接。
- 2. 当有客户端连接成功时,获取客户端的 IP 地址。
- 3. 通过输入流读取客户端发送的问题,并在预先定义的 `HashMap` 中查找对应的答案。如果问题不存在于 `HashMap` 中,则返回默认的回答“对不起,我不知道你在说什么!”。
- 4. 将找到的答案通过输出流发送回客户端。
客户端 `AIzzClient` 主要负责:
- 1. 从控制台读取用户输入的问题。
- 2. 创建与本地主机 `8848` 端口的连接。
- 3. 通过输出流向服务端发送问题。
- 4. 发送完成后关闭输出流。
通过输入流接收服务端返回的答案,并打印输出。 两者通过 TCP 协议进行通信,客户端发送问题,服务端接收并处理后返回答案,形成了一个简单的交互流程。例如,当用户在客户端输入“你好”,客户端将这个问题发送给服务端,服务端从 `HashMap` 中找到对应的“你好呀,孙子”并返回给客户端,客户端接收并显示出来。
补充:
输入流(InputStream):
输入流用于从数据源读取数据到程序中。数据源可以是文件、网络连接、内存缓冲区等。
它提供了一系列方法,允许程序按顺序读取数据的一部分或全部。常见的输入流操作包括读取一个字节、读取一组字节、读取一个字符、读取一行文本等。
例如,FileInputStream
可以从文件中读取字节数据,BufferedReader
可以更高效地从字符输入流中读取文本行。
输出流(OutputStream):
输出流则用于将程序中的数据写入到数据目的地。目的地同样可以是文件、网络连接、内存缓冲区等。
输出流提供了方法来将数据按顺序写入,例如写入一个字节、写入一组字节、写入一个字符等。
比如,FileOutputStream
用于向文件中写入字节数据,PrintWriter
常用于向输出流中写入格式化的文本。
总的来说,输入流和输出流是 Java 中用于实现数据在程序与外部数据源或目的地之间传输的重要机制。通过合理使用不同类型的输入流和输出流,可以方便地处理各种数据的读取和写入操作。
`BufferedReader` 是如何提高读取效率的?
`BufferedReader` 通过内部的缓冲区来提高读取效率。 当使用普通的输入流(如 `FileReader` )读取数据时,每次读取操作可能都会导致与底层数据源(例如文件)的直接交互,这可能会带来较高的系统开销,特别是在频繁读取小量数据的情况下。
而 `BufferedReader` 在其内部维护了一个缓冲区(通常是一个字节数组)。当调用读取方法时,它首先尝试从缓冲区中获取数据。如果缓冲区中有足够的数据,就直接返回,避免了频繁地与底层数据源进行交互。 当缓冲区中的数据不足时,`BufferedReader` 会一次性从底层输入流中读取较大块的数据填充缓冲区,而不是每次只读取一个字节或几个字节。
例如,如果缓冲区大小设置为 8192 字节,那么 `BufferedReader` 可能会一次性从底层输入流读取 8192 字节的数据放入缓冲区,后续的读取操作只要从缓冲区获取即可,大大减少了与底层输入流交互的次数,从而显著提高了读取效率。 这种缓冲机制特别适用于按行读取文本文件的情况,因为按行读取时,可能需要多次读取少量的数据,`BufferedReader` 的缓冲作用能有效地优化这种操作。
`BufferedWritter` 是如何提高读取效率的?
`BufferedWriter` 主要通过内部的缓冲区来提高写入效率。 当使用普通的字符输出流(如 `FileWriter`)进行写入操作时,每次写入一个字符或一小段数据,都可能会引发与底层输出目的地(例如文件)的实际交互,这会带来较多的系统开销。
而 `BufferedWriter` 在其内部维护了一个缓冲区(通常是一个字符数组)。当调用写入方法(如 `write`)时,数据并不是直接写入到目的地,而是先被存储到缓冲区中。 缓冲区有一定的大小,当缓冲区被填满或者手动调用 `flush` 方法时,`BufferedWriter` 会一次性地将缓冲区中的数据写入到底层输出流中。这样就减少了与底层输出目的地交互的次数,从而显著提高了写入效率。
例如,在向文件写入大量数据时,如果不使用 `BufferedWriter`,可能需要频繁地进行磁盘 I/O 操作,而使用 `BufferedWriter` 后,只需在缓冲区满或主动刷新时才进行实际的磁盘写入,大大降低了磁盘 I/O 的次数。 一般情况下,使用默认大小的缓冲区即可满足大多数需求。但如果明确知道写入数据的特征,也可以在创建 `BufferedWriter` 对象时指定缓冲区的大小,