Java Web入门

Java Web入门

在这里我们可以学习Java程序与另一台机器上的程序对话。Java好处之一就是:通过网络发送和接收数据都是I/O,只是I/O链末尾的连接稍有不同。我们可以建立一个client类和一个server类来模拟服务器与客户端的数据传递。

传输协议

TCP(传输控制协议)和UDP(用户数据报协议)是两种常用的传输层协议,它们在计算机网络中负责在主机之间传输数据。它们各自有不同的特性和适用场景。以下是对它们的详细介绍:

TCP(传输控制协议)

  • 连接导向: TCP是一个面向连接的协议,这意味着在实际数据传输之前,必须在通信的两端建立一个连接。这个连接确保数据的可靠传输。
  • 可靠性: TCP提供可靠的数据传输,确保数据包的顺序、完整性和无丢失。在数据传输过程中,TCP会进行错误检测、重传丢失的数据包、以及保证数据包的顺序正确。
  • 流量控制: TCP通过流量控制机制来避免发送方过快地发送数据,从而防止接收方处理不及。
  • 拥塞控制: TCP还具备拥塞控制功能,能够动态调整数据传输的速率,以应对网络中的拥塞情况。
  • 数据传输: TCP将数据分段,并在接收端重新组装成完整的数据流。每个数据包都有序号,以便接收端可以按顺序重新组装数据。
  • 使用场景: 由于其可靠性,TCP适用于需要保证数据传输完整性和顺序的应用,如网页浏览(HTTP/HTTPS)、电子邮件(SMTP)、文件传输(FTP)等。

UDP(用户数据报协议)

  • 无连接: UDP是一个无连接的协议,这意味着在数据传输之前不需要建立连接。每个数据包(称为数据报)独立发送,不保证数据的到达顺序或完整性。
  • 不可靠: UDP不提供数据包丢失的重传、顺序保证或错误校验。数据可能会丢失、重复或乱序到达。
  • 低开销: 由于UDP没有连接建立和管理的开销,相比TCP,UDP的开销更小,传输效率更高。
  • 数据传输: UDP将数据分割成数据报,每个数据报独立发送,接收端不进行重新组装,接收应用需要自行处理数据的顺序和完整性。
  • 使用场景: UDP适用于那些对数据传输速度要求高而对可靠性要求低的应用,如实时视频流、在线游戏、语音通话(VoIP)等。在这些场景中,数据传输的速度和时效性比数据的完整性更为重要。

所以一般情况下,我们使用TCP协议传输较多。

关于数据传输,我们可以参考下图的TCP三次握手,当我们完成了三次握手,就说明客户端已经和服务器建立了连接,可以进行数据传输了。

tcp三次连接握手

通俗的理解:

  • 客户端:听得到吗? ——> 服务器 (连接)
  • 客户端 ——> 服务器:听得到,你听的到我吗? (接收)
  • 客户端:可以听到,我们可以说话了 ——> 服务器 (发送)

Web实现思路

连接

要建立连接,我们需要知道这个服务器的IP地址和TCP端口号。当我们发送信息时Java会将二进制数据传入“网络栈”中,并向外发送请求。

我们大致的连接思路如下:

1
2
InetSocketAddress serverAddress = new InetSocketAddress("192.164.1.103",5000);
SocketChannel socketChannel = SocketChannel.open(serverAddress);

通过InetSocketAddress我们记录了所要地址的ip和端口,然后我们通过SocketChannel进行连接。注意在SocketChannel后面是SocketChannel.open(serverAddress),说明我们并没有使用构造器来创建,而是调用了一个open()方法。这会创建一个新的SocketChannel,并把它连接到一个提供的地址。

注意:一般情况下TCP端口号在0-1023是用作公认服务的,当自己在创建服务器的时候应当尽量避免这些端口。我们使用的端口可以在1024-65535之间随便选择。

接收

用BufferedReader读取

BufferedReader通过使用缓冲区来减少对底层输入流的频繁访问,从而提高读取效率。它通常会将多个字符读取到内存中,然后逐个读取,从而减少I/O操作的开销.

建立与服务器的连接(就是重复上一步):

1
2
SocketAddress serverAddr = new InetSocketAddress("127.0.0.1",5000);
SocketChannel socketChannel = SocketChannel.open(serverAddr);

从这个连接创建或获取一个Reader:

1
Reader reader = Channels.newReader(socketChannel,StandardCharsets.UTF_8);

这个Reader是字符流和字节流交流的桥梁,比如来自Channel的流就是字节流,链流顶端的BufferedReader就是字符流。我们在文件传输最底层是由字节流传递的,而最后由我们客户端接收显示就是要由字符流形成。也就是我们用reader将字节变成了字符。

创建一个BufferedReader并读取:

1
2
BufferedReader bufferedReader = new BufferedReader(reader);
String message = bufferedReader.readLine();

我们用我们上一步的reader,我们知道reader此时已经是字符,我们只需再将其通过BufferedReader构造器,变成缓冲的字符,然后我们就可以再客户端读取数据了。

发送

用PrintWriter写至网络

同样的线创建一个连接:

1
2
SocketAddress serverAddr = new InetSocketAddress("127.0.0.1",5000);
SocketChannel socketChannel = SocketChannel.open(serverAddr);

从连接创建或获得一个Writer:

1
Writer writer = Channels.newWriter(socketChannel,StandardCharsets.UTF_8);

Writer也同Reader一样,在字符流和字节流中间充当桥梁。

创建一个PrinterWriter并打印一些内容:

1
2
3
PrintWriter printWriter = new PrintWriter(writer);
writer.println("message to send");
writer.println("another message");

PrintWriter接收字符,然后串链到Writer,最后Writer转换字符流为字节流传递给服务器。

另一种连接方式

Socket连接:

1
2
3
4
5
6
7
8
9
10
Socket chatSocket = new Socket("127.0.0.1",5000);
InputStreamReader in = new InputStreamReader(chatSocket.getInputStream());

BufferedReader bufferedReader = new BufferedReader(in);
String message = bufferedReader.readLine();

PrintWriter writer = new PrintWriter(chatSocket.getOutputStream());

writer.println("message to send");
writer.println("another message");

当然我们已经有了Channel来进行连接了,为什么还要用Socket来连接呢?

  1. 基本概念
  • Socket:是Java中用于进行网络通信的传统方式。它代表一个端点,通过TCP或UDP协议在网络中进行数据传输。
  • Channel:是Java NIO(New Input/Output)中的一个概念,代表一个连接的打开的通道,支持非阻塞I/O操作,能够进行更高效的文件和网络操作。
  1. 阻塞与非阻塞
  • Socket:通常是阻塞的,这意味着当你进行读写操作时,调用会一直等待,直到操作完成。
  • Channel:支持非阻塞模式,允许你在没有数据可用时继续执行其他操作。可以通过Selector管理多个Channel,从而在单个线程中处理多个连接。
  1. I/O模型
  • Socket:使用传统的字节流或字符流进行数据的读取和写入。
  • Channel:使用Buffer进行数据的传输,可以通过ByteBuffer和CharBuffer等直接操作内存,提高性能。
  1. API设计
  • Socket:使用较简单,适合小型或简单的网络应用。比如,通过SocketServerSocket类进行创建和管理连接。
  • Channel:提供更灵活的API,如SocketChannelServerSocketChannel等,适合高性能应用,尤其是需要处理大量并发连接的场景。
  1. 适用场景
  • Socket:适合小型应用或较为简单的网络通信需求。
  • Channel:适合高性能的网络应用,尤其是在处理大量连接或需要非阻塞I/O的场景。

Socket适合传统、简单的网络编程,而Channel则是为了更高效的非阻塞I/O设计的,特别是在需要处理高并发和高性能时。

两个实例

服务器向客户端发送字符串

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.bayeeaa.demo1.web.Advice;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;

public class DaliyAdviceClient {
public void go() {
InetSocketAddress serverAddress = new InetSocketAddress("127.0.0.1",5000); //获取地址和端口
try(SocketChannel socketChannel = SocketChannel.open(serverAddress)){ //尝试连接,返回字节流,赋值于socketChannel
Reader channelReader = Channels.newReader(socketChannel, StandardCharsets.UTF_8); //channelReader读入传输数据,转为字符流
BufferedReader reader = new BufferedReader(channelReader); //BufferedReader可以显著提高读取性能,它通过在内存中维护一个缓冲区来减少对底层数据源的访问频率
String advice = reader.readLine(); //读取行
System.out.println("Today you should " + advice);
reader.close(); //关闭输入流
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static void main(String[] args) {
new DaliyAdviceClient().go();
}
}

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.bayeeaa.demo1.web.Advice;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.nio.channels.Channel;
import java.nio.channels.Channels;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Random;

public class DaliyAdviceServer {
final private String[] adviceList = { //随机一个建议
"Happy",
"Good",
"Be yourself",
"relax",
"paly computer games"
};
private final Random random = new Random();

private String getAdvice() {
int nextAdvice = random.nextInt(adviceList.length);
return adviceList[nextAdvice];
}

public void go(){
try(ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
serverChannel.bind(new InetSocketAddress(5000));
while(serverChannel.isOpen()){
SocketChannel clientChannel = serverChannel.accept();
PrintWriter writer = new PrintWriter(Channels.newOutputStream(clientChannel));
String advice = getAdvice();
writer.println(advice);
writer.close();
System.out.println(advice);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static void main(String[] args) {
new DaliyAdviceServer().go();
}
}

客户端向服务器发送字符串信息

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.bayeeaa.demo1.web.simplechat;

import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.nio.channels.Channels;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;

public class SimpleChatClientA {
private JTextField outgoing;
private PrintWriter writer;
public void go() {
setUpNetworking();

outgoing = new JTextField(20); //这里是在搞GUI

JButton sendButton = new JButton("Send");
sendButton.addActionListener(e -> sendMessage());

JPanel mainPanel = new JPanel();
mainPanel.add(outgoing);
mainPanel.add(sendButton);
JFrame frame = new JFrame("Simple Chat Client");
frame.getContentPane().add(BorderLayout.CENTER,mainPanel);
frame.setSize(400,100);
frame.setVisible(true);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
}

public void setUpNetworking() {
InetSocketAddress serverAddress = new InetSocketAddress("127.0.0.1",5000);
try {
SocketChannel socketChannel = SocketChannel.open(serverAddress); //socketChannel为网络通道
writer = new PrintWriter(Channels.newWriter(socketChannel, StandardCharsets.UTF_8)); //PrintWriter 是一个字符流类,用于将字符数据写入输出流
//Channels.newWriter(socketChannel, StandardCharsets.UTF_8) 创建了一个 WritableByteChannel 的字符流,这样可以将字符数据写入到与 socketChannel 关联的网络通道中。socketChannel 是一个与远程服务器连接的 SocketChannel。
System.out.println("Network established");
writer.println("nihao");//这里测试,是将"nihao"输入到了服务器
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private void sendMessage() {
writer.println(outgoing.getText());
writer.flush(); //强制将所有缓存的输出数据写入到底层输出流中
outgoing.setText(""); //setText 是一个方法,用于设置文本组件中显示的文本内容.这里是清空 outgoing 组件中的文本
outgoing.requestFocus(); //requestFocus() 方法会将焦点设置到 outgoing 组件,使得用户可以立即开始在该组件中输入文本
}

public static void main(String[] args) {
new SimpleChatClientA().go();
}
}

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.bayeeaa.demo1.web.simplechat;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.nio.channels.Channels;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SimpleChatServer {
private final List<PrintWriter> clientWriters = new ArrayList<>();

private void tellEveryone(String message) {
for(PrintWriter writer : clientWriters){
writer.println(message);
writer.flush();
}
}

public class ClientHanlder implements Runnable{
BufferedReader reader;
SocketChannel socket;

public ClientHanlder(SocketChannel clientSocket){
socket = clientSocket;
reader = new BufferedReader(Channels.newReader(socket, StandardCharsets.UTF_8));
}

@Override
public void run() {
String message;
try {
while((message = reader.readLine()) != null){
System.out.println("read " + message);
tellEveryone(message);
}
}catch (IOException ex){
ex.printStackTrace();
}

}
}

public void go() {
ExecutorService treadPool = Executors.newCachedThreadPool();
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(5000));

while (serverSocketChannel.isOpen()) {
SocketChannel clientSocket = serverSocketChannel.accept();
PrintWriter writer = new PrintWriter(Channels.newWriter(clientSocket, StandardCharsets.UTF_8));
clientWriters.add(writer);
treadPool.submit(new ClientHanlder(clientSocket));
System.out.println("got a connection");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static void main(String[] args) {
new SimpleChatServer().go();
}
}

这里使用了简单的GUI使操作可视化,只需在跳出的窗口中输入字符串就可以发送到服务器,并在其终端查看内容。

实例如图

可以看到”nihao”和我刚刚输入的”123”是同时打出的,说明这个”nihao”是加入了缓冲中,然后点击”send”时最后释放。


Java Web入门
https://bayeeaa.github.io/2024/09/22/Java-Web入门/
Author
Ye
Posted on
September 22, 2024
Licensed under