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三次握手,当我们完成了三次握手,就说明客户端已经和服务器建立了连接,可以进行数据传输了。
通俗的理解:
- 客户端:听得到吗? ——> 服务器 (连接)
- 客户端 ——> 服务器:听得到,你听的到我吗? (接收)
- 客户端:可以听到,我们可以说话了 ——> 服务器 (发送)
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来连接呢?
- 基本概念
- Socket:是Java中用于进行网络通信的传统方式。它代表一个端点,通过TCP或UDP协议在网络中进行数据传输。
- Channel:是Java NIO(New Input/Output)中的一个概念,代表一个连接的打开的通道,支持非阻塞I/O操作,能够进行更高效的文件和网络操作。
- 阻塞与非阻塞
- Socket:通常是阻塞的,这意味着当你进行读写操作时,调用会一直等待,直到操作完成。
- Channel:支持非阻塞模式,允许你在没有数据可用时继续执行其他操作。可以通过Selector管理多个Channel,从而在单个线程中处理多个连接。
- I/O模型
- Socket:使用传统的字节流或字符流进行数据的读取和写入。
- Channel:使用Buffer进行数据的传输,可以通过ByteBuffer和CharBuffer等直接操作内存,提高性能。
- API设计
- Socket:使用较简单,适合小型或简单的网络应用。比如,通过
Socket
和ServerSocket
类进行创建和管理连接。
- Channel:提供更灵活的API,如
SocketChannel
、ServerSocketChannel
等,适合高性能应用,尤其是需要处理大量并发连接的场景。
- 适用场景
- 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)){ Reader channelReader = Channels.newReader(socketChannel, StandardCharsets.UTF_8); BufferedReader reader = new BufferedReader(channelReader); 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);
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); writer = new PrintWriter(Channels.newWriter(socketChannel, StandardCharsets.UTF_8)); System.out.println("Network established"); writer.println("nihao"); } catch (IOException e) { throw new RuntimeException(e); } }
private void sendMessage() { writer.println(outgoing.getText()); writer.flush(); outgoing.setText(""); outgoing.requestFocus(); }
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”时最后释放。