文章大纲来自于网络:使用Socket进行数据传输_哔哩哔哩_bilibili
1.Server and client
Socket 就像是两个人之间的电话线。
- 当一个人想要与另一个人通话,他们会拿起电话(创建一个Socket)。
- 两人之间可以通过这条电话线(Socket)传递信息,无论是语言、文字还是其他形式的数据。
- 通话结束后,他们可以挂断电话(关闭Socket),使得两者之间的通信中断。
在这个比喻中,电话线(Socket)提供了一种通信的方式,使得两个人可以在不同的地方进行交流。同样地,Socket 允许计算机在网络上进行通信。
首先,我们创建一个类表示服务器,在这个服务器里,我们希望和客户端进行联系,那我们肯定需要在服务器里接受通过socket传输过来的客户端类的对象。
所以我们先在server类里创建一个socket来表示服务器本身,并给这个服务器分配8080端口:
ServerSocket server = new ServerSocket(8080)
然后调用ServerSocket类的accept方法,接受通过socket传输过来的内容,这个对象就是对客户端的抽象:
Socket socket1=server.accept();
当没有客户端链接的时候,accept方法会一直阻塞线程。
这个socket里包含一系列客户端的信息,我们可以打印出来(第一个get方法是取socket的地址,第二个get方法是将这个地址转化为string类型):
System.out.println("IP address:"+socket1.getInetAddress().getHostAddress());
现在我们已经完成了对客户端的操作,需要关闭服务器。如果不关闭服务器,服务器就会一直占用这个8080端口 。
我们可以这样写,手动关闭server
public class Server {
public static void main(String args[]){
try {
ServerSocket server = new ServerSocket(8080);
//accept() will block the thread until the clint send a request
// accept() will return a socket represent the client
System.out.println("connecting...");
Socket socket1=server.accept();
System.out.println("IP address:"+socket1.getInetAddress().getHostAddress());
//If you do not close the server, the port will be occupied and you are not able to use the port
server.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
也可以这样写进try catch语句,自动关闭:
public static void main(String args[]){
try(ServerSocket server = new ServerSocket(8080)) {
//accept() will block the thread until the clint send a request
// accept() will return a socket represent the client
System.out.println("connecting...");
Socket socket1=server.accept();
System.out.println("IP address:"+socket1.getInetAddress().getHostAddress());
//If you do not close the server, the port will be occupied and you are not able to use the port
}catch(IOException e){
e.printStackTrace();
}
}
}
try后括号里的内容需要实现autocloseable 接口。
AutoCloseable
是 Java 编程语言中的一个接口(Interface)。这个接口主要用于支持 Java 7 引入的 "try-with-resources" 语句。大致作用是,如果我们在try中new了一个实现了AutoCloseable
接口的对象,java会帮我们自动关闭这个对象。
client
新建一个socket对象表示client本身:
Socket socket = new Socket("localhost",8080)
localhost是主机的ip地址,8080是端口。在创建client对象socket的时候,已经和服务器绑定好了。
同时,这个socket也需要被自动关闭,而socket类也实现了autocloseable接口,所以我们可以直接放进try语句。
import java.io.IOException;
import java.net.Socket;
public class Client {
public static void main(String[] args) {
//if you run on your own computer, then you can use "localhost"
//if you run in the local area network(LAN), use "ipconfig" to see your source
try(Socket socket = new Socket("localhost",8080)){
//here is the same way as Server
//socket should close and I write it in try block
}catch(IOException e){
e.printStackTrace();
}
}
}
先运行server,再运行client,server在client运行之前,会一直监听端口。
这实际上是tcp的握手过程,只不过java全部封装了。
接受多个client
用while循环就行了
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String args[]){
try(ServerSocket server = new ServerSocket(8080)) {
//accept() will block the thread until the clint send a request
// accept() will return a socket represent the client
System.out.println("connecting...");
while (true){
Socket socket1=server.accept();
System.out.println("IP address:"+socket1.getInetAddress().getHostAddress());
}
//If you do not close the server, the port will be occupied and you are not able to use the port
}catch(IOException e){
e.printStackTrace();
}
}
}
这里的client和server是怎么联系的呢?
我们在client里创建了一个socket,给这个socket指定端口8080,然后我们在server里创建一个
ServerSocket,通过监听8080端口来连接这个socket。同时,把这个监听到的socket又通过accept接受为一个本地的socket,在例子里命名为socket1.这个本地化的socket1其实和client里的socket是连接的,但是名字可以不一样。
-
server.accept()
:这是在服务器端Socket编程中的一种操作,其中server
是一个ServerSocket对象。ServerSocket对象通常在服务器端用于监听特定的网络端口,以便接受客户端的连接请求。accept()
方法是一个阻塞调用,它会等待客户端的连接请求,一旦有客户端连接请求到达,它会接受这个连接请求,并返回一个新的Socket对象,该Socket对象代表与客户端建立的连接。 -
Socket socket1=server.accept();
:这行代码执行了accept()
方法,并将返回的Socket对象存储在变量socket1
中。这个socket1
对象表示与一个特定客户端建立的连接,可以使用它进行双向通信,即从客户端接收数据和向客户端发送数据。
2.客户端发送数据
client
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;
public class Client {
public static void main(String[] args) {
//if you run on your own computer, then you can use "localhost"
//if you run in the local area network(LAN), use "ipconfig" to see your source
try(Socket socket = new Socket("localhost",8080);
Scanner scanner=new Scanner(System.in)){ //Scanner also need to close, so write scanner in try block
System.out.println("Already connected to the server!");
System.out.println("write something that I can send to server:");
//this is a convert stream
// it is more convenient than socket.getOutputStream()
OutputStreamWriter writer=new OutputStreamWriter(socket.getOutputStream());
writer.write(scanner.nextLine()+"\n");
//flush and client will send it to server
writer.flush();
//here is the same way as Server, "socket" should close and I write it in try block
System.out.println("data already sent");
}catch(IOException e){
System.out.println("fail to connect to server");
e.printStackTrace();
}
}
}
I/O 1
注意
OutputStreamWriter writer=new OutputStreamWriter(socket.getOutputStream()); writer.write(scanner.nextLine()+"\n");
OutputStreamWriter
是 Java 中用于将字符写入输出流的类,它是字符流与字节流之间的桥梁。具体而言,OutputStreamWriter
可以将字符按照指定的字符编码转换为字节,并写入到底层的输出流中。
以下是创建 OutputStreamWriter
对象的一般形式:
OutputStreamWriter writer = new OutputStreamWriter(OutputStream out, Charset charset);
其中:
out
是底层的输出流,例如socket.getOutputStream()
返回的输出流。charset
是指定的字符编码,例如StandardCharsets.UTF_8
。
上面的两行代码创建了一个OutputStreamWriter对象,该对象连接到给定的套接字(socket)的输出流,以便将字符数据写入该套接字,所以我可以向OutputStreamWriter写入数据,写的数据会被放到套接字(socket)的输出流。
想象一个水管,本来的一头是套接字(socket)的输出流,但是输入的水不能直接从这种头输入水管,所以我们要给水管换一个头,就是这个OutputStreamWriter,然后我们从这个新头往水管里加水(writer.write(scanner.nextLine()+"\n"))
I/O 2
ChatGPT
ChatGPT
字节流(Byte Streams)和字符流(Character Streams)是用于处理输入和输出的两种基本的I/O流,它们之间的主要区别在于处理的数据类型和应用场景。
1. 字节流(Byte Streams):
-
数据处理单位: 字节流以字节为单位处理数据。
InputStream
和OutputStream
是字节流的主要类。 -
用途: 适用于处理二进制数据,如图像、音频、视频等。它们不关心数据的具体内容,而是简单地处理字节序列。
-
读写: 字节流通常用于读写字节,不考虑字符编码。它们提供了
read()
和write()
等方法来逐字节读写数据。 -
底层: 字节流是连接到底层输入和输出设备(例如文件、网络连接)的抽象,可以处理任何类型的数据。
2. 字符流(Character Streams):
-
数据处理单位: 字符流以字符为单位处理数据。
Reader
和Writer
是字符流的主要类。 -
用途: 适用于处理文本数据,字符流会考虑字符的编码。它们提供了更高层次的抽象,可以直接读取和写入字符。
-
读写: 字符流用于读写字符,考虑字符的编码和字符集。它们提供了
read()
和write()
方法,可以按字符而不是字节来读写数据。 -
底层: 字符流通常建立在字节流之上,使用
InputStreamReader
和OutputStreamWriter
进行字节到字符的转换。
总结:
- 如果处理的是二进制数据或不考虑字符编码,例如处理图像、音频等,使用字节流更合适。
- 如果处理的是文本数据,并且需要考虑字符编码,例如读写文本文件,使用字符流更合适。
在实际应用中,根据处理的数据类型和需求来选择合适的流。在处理文本数据时,字符流通常更方便,因为它们处理字符编码和换行符等细节,而在处理二进制数据时,字节流更为直接。
demo
使用字节流处理二进制文件:
注意这里用的时ascii码
import java.io.*;
public class ByteStreamDemo {
public static void main(String[] args) {
// 写入二进制文件
try (OutputStream outputStream = new FileOutputStream("binary_data.bin")) {
byte[] data = { 65, 66, 67, 68, 69 }; // ASCII codes for 'A', 'B', 'C', 'D', 'E'
outputStream.write(data);
} catch (IOException e) {
e.printStackTrace();
}
// 读取二进制文件
try (InputStream inputStream = new FileInputStream("binary_data.bin")) {
int byteData;
while ((byteData = inputStream.read()) != -1) {
System.out.print((char) byteData + " ");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用字符流处理文本文件:
这里直接使用字符本身
import java.io.*;
public class CharacterStreamDemo {
public static void main(String[] args) {
// 写入文本文件
try (Writer writer = new FileWriter("text_data.txt")) {
String textData = "Hello, World!\nThis is a text file.";
writer.write(textData);
} catch (IOException e) {
e.printStackTrace();
}
// 读取文本文件
try (Reader reader = new FileReader("text_data.txt")) {
int charData;
while ((charData = reader.read()) != -1) {
System.out.print((char) charData);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
server
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class Server {
public static void main(String args[]){
try(ServerSocket server = new ServerSocket(8080);) {
//accept() will block the thread until the clint send a request
// accept() will return a socket represent the client
System.out.println("connecting...");
while (true){
Socket socket1=server.accept();
System.out.println("IP address:"+socket1.getInetAddress().getHostAddress());
System.out.println("reading data...");
//BufferedReader easy to output the data
BufferedReader reader=new BufferedReader(new InputStreamReader(socket1.getInputStream()));
//socket need to close and did not write in try block
//so need to close here
System.out.println(reader.readLine());
socket1.close();
}
//If you do not close the server, the port will be occupied and you are not able to use the port
}catch(IOException e){
e.printStackTrace();
}
}
}
同理
BufferedReader reader=new BufferedReader(new InputStreamReader(socket1.getInputStream()));
I/O 3
InputStreamReader
是 Java 中用于从输入流读取字符的类,它是字符流与字节流之间的桥梁。InputStreamReader
将字节从底层的输入流转换为字符,并根据指定的字符编码来解码字节。
BufferedReader
是 Java 中用于从字符输入流读取文本的类,它可以提供缓冲功能,从而提高读取性能。缓冲功能是指在读取或写入数据时,将数据暂时存储在内存中的缓冲区(buffer)中,而不是每次读取或写入一个字节或字符就进行实际的IO操作。使用缓冲技术可以减少实际的IO次数,从而提高读写性能。同时BufferedReader提供了很多方法让我们更方便地打印数据。
这行代码创建了一个BufferedReader对象,该对象连接到给定的套接字(socket1)的输入流,以便以字符流的方式从套接字中读取数据
3.服务端发送数据到客户端
server:
// 这行代码创建了一个OutputStreamWriter对象,该对象连接到给定的套接字(socket)的输出流
// 以便将字符数据写入该套接字,所以我可以向OutputStreamWriter写入数据
// 写的数据会被放到套接字(socket)的输出流。
OutputStreamWriter writer=new OutputStreamWriter(socket1.getOutputStream());
writer.write("as a client, you have sent "+str);
writer.flush();
client:
//这行代码创建了一个BufferedReader对象,该对象连接到给定的套接字(socket1)的输入流
// 以便以字符流的方式从套接字中读取数据B
//其实InputStreamReader就已经可以接收到数据了,但是不好打印
//所以我们又嵌套了一层BufferedReader
//因为输入的时候不需要打印,所以直接用OutputStreamWriter就行,少了一层嵌套
BufferedReader reader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("data sent form server "+reader.readLine());
4.其他
设置超时时间
为了防止客户端迟迟不给服务端发送数据导致的服务端阻塞,我们可以通过
socket1.setSoTimeout(3000);
设置一个时间,超过这个时间服务端就会抛出一个异常。
如果通过 socket1
的输入流进行读取时,在 3 秒内没有可读取的数据,将抛出 SocketTimeoutException
异常。
手动将客户端连接服务器
之前我们都是通过构造方法,在new的时候就直接将客户端socket和服务器连接,我们也可以先不连接,在后面用connect方法显式连接。
1000设置了超时时间,如果超出1秒钟还没有连接,就会抛出一个异常
//here we use construct method to connect to the server
// but if we want to connect manually
// we can use connect method
socket.connect(new InetSocketAddress("localhost",8080),1000);
保持连接的活跃
有时候我们虽然在客户端和服务端之间建立的连接,但是服务器出现了严重的错误,已经崩溃。此时我们的客户端并不知道,依然保持连接,这样浪费了资源。所以我们可以用setKeepAlive方法,来定期向服务器发送数据,如果服务器没有相应,我们就知道服务器崩溃了。 后续是否会自动关闭连接取决于机器本身的协议。
socket.setKeepAlive(true);
client:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Scanner;
public class Client {
public static void main(String[] args) {
//if you run on your own computer, then you can use "localhost"
//if you run in the local area network(LAN), use "ipconfig" to see your source
try(Socket socket = new Socket("localhost",8080);
Scanner scanner=new Scanner(System.in)){ //Scanner also need to close, so write scanner in try block
// setKeepAlive(true) 是 Socket 类的一个方法,用于设置 TCP 连接的保持活动状态(keep-alive)。
// 当启用 TCP 连接保持活动状态时,系统将定期发送小的探测数据包(keep-alive 消息)到远程主机,以确保连接仍然保持活跃。
socket.setKeepAlive(true);
//here we use construct method to connect to the server
// but if we want to connect manually
// we can use connect method(the following line is not necessary)
socket.connect(new InetSocketAddress("localhost",8080),1000);
System.out.println("Already connected to the server!");
System.out.println("write something that I can send to server:");
//this is a convert stream
// it is more convenient than socket.getOutputStream()
OutputStreamWriter writer=new OutputStreamWriter(socket.getOutputStream());
writer.write(scanner.nextLine()+"\n");
//flush and client will send it to server
writer.flush();
//here is the same way as Server, "socket" should close and I write it in try block
System.out.println("data already sent");
//这行代码创建了一个BufferedReader对象,该对象连接到给定的套接字(socket1)的输入流
// 以便以字符流的方式从套接字中读取数据B
//其实InputStreamReader就已经可以接收到数据了,但是不好打印
//所以我们又嵌套了一层BufferedReader
//因为输入的时候不需要打印,所以直接用OutputStreamWriter就行,少了一层嵌套
BufferedReader reader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("data sent form server "+reader.readLine());
}catch(IOException e){
System.out.println("fail to connect to server");
e.printStackTrace();
}
}
}
server:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class Server {
public static void main(String args[]){
try(ServerSocket server = new ServerSocket(8080);) {
//accept() will block the thread until the clint send a request
// accept() will return a socket represent the client
System.out.println("connecting...");
while (true){
//这里接受的并不是唯一的客户端,任何多个客户端都可以通过8080端口连接服务器
Socket socket1=server.accept();
//如果通过 socket1 的"输入流"进行读取时,在 3 秒内没有可读取的数据,将抛出 SocketTimeoutException 异常。
socket1.setSoTimeout(3000);
System.out.println("IP address:"+socket1.getInetAddress().getHostAddress());
System.out.println("reading data...");
//这行代码创建了一个BufferedReader对象,该对象连接到给定的套接字(socket1)的输入流
// 以便以字符流的方式从套接字中读取数据B
BufferedReader reader=new BufferedReader(new InputStreamReader(socket1.getInputStream()));
//socket need to close and did not write in try block
//so need to close here
String str=reader.readLine();
System.out.println(str);
// 这行代码创建了一个OutputStreamWriter对象,该对象连接到给定的套接字(socket)的输出流
// 以便将字符数据写入该套接字,所以我可以向OutputStreamWriter写入数据
// 写的数据会被放到套接字(socket)的输出流。
OutputStreamWriter writer=new OutputStreamWriter(socket1.getOutputStream());
writer.write("as a client, you have sent "+str);
writer.flush();
socket1.close();
}
//If you do not close the server, the port will be occupied and you are not able to use the port
}catch(IOException e){
e.printStackTrace();
}
}
}