1.UDP和TCP
UDP和TCP是传输层协议中最核心的两种协议.
特点:
UDP: 无连接,不可靠传输,面向数据报,全双工
TCP: 有连接,是可靠传输,面向字节流,全双工
1.1 有无连接
①有连接:
打电话时,直接对方接通,才能算是完成连接建立.
②无连接:
发信息时,不需要建立连接.
客户端和服务器之间,使用"本本"(内存)保存对端的信息,双方都保存这个信息,此时"连接"就出现了.
1.2 是否可靠传输
①可靠传输:
指的是A尽可能地把消息传给B,并且在传输失败地时候,A能感知到,并不是说A给B地消息100%能到.传输效率较低.
②不可靠传输:
发送出去就不管了.传输效率较高.
问:
TCP是可靠传输,UDP是不可靠传输,因此TCP比UDP更安全?
这个说法是错误的.
"网络安全"指的是,传输的数据是否容易被黑客截获,被截获后是否会泄露一些重要信息.
1.3 面向字节流/面向数据报
①面向字节流:
以字节为单位进行接发,TCP和文件操作类似,都是"流"式的.
通过TCP读写100字节数据,可以一次读写100字节,也可以一次读写50字节,分两次读.
②面向数据报:
以数据报为单位进行接发.
1.4 全双工/半双工
①全双工:
一个通道,可以双向通信.
比如:
马路,网线.
②半双工:
一个通道,只能单向通信.
2.实现UDP版本的客户端和服务器
UDP提供的api有两个核心的类:DatagramSocket.DatagramPacket.
2.1 DatagramSocket
DatagramSocket是一个Socket对象.用于发送和接收UDP数据报.
注意:在操作系统中,socket会当做文件来处理,会存放到文件描述符表中
要进行网络通信,必须得先有Socket对象.
举个例子,当我们在食堂吃饭的时候,每个打饭的窗口都是有固定的位置的,这里的窗口就相当于一个服务器,有固定的端口号,方便被我们找到,而我们坐的位置就相当于客户端的端口号.
但是由于我们每次坐的位置不同,所以客户端的端口号就不能指定固定值.
所以,一个客户端的主机,上面运行的程序很多,无法知道手动指定的端口是不是被其它程序占用了,进而让系统自动分配一个端口就是明智的选择.
2.2 DatagramPacket
表示了一个UDP数据报,代表了系统中设定的UDP数据报的二进制结构.
2.3 UDP回显服务器
回显服务器:客户端发什么,服务器就返回什么.
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
// UDP 的 回显服务器.
// 客户端发的请求是啥, 服务器返回的响应就是啥.
public class UdpEchoServer {
private DatagramSocket socket = null;
// 参数是服务器要绑定的端口
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
// 使用这个方法启动服务器.
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 反复的, 长期的执行针对客户端请求处理的逻辑.
// 一个服务器, 运行过程中, 要做的事情, 主要是三个核心环节.
// 1. 读取请求, 并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
// 这样的转字符串的前提是, 后续客户端发的数据就是一个文本的字符串.
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 根据请求, 计算出响应
String response = process(request);
// 3. 把响应写回给客户端
// 此时需要告知网卡, 要发的内容是啥, 要发给谁.
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
// 记录日志, 方便观察程序执行效果.
System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(), requestPacket.getPort(),
request, response);
}
}
// 根据请求计算响应. 由于是回显程序, 响应内容和请求完全一样.
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
注意:
①
②
③
④
⑤
2.4 UDP客户端
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
// 服务器的 ip 和 服务器的端口.
public UdpEchoClient(String ip, int port) throws SocketException {
serverIp = ip;
serverPort = port;
// 这个 new 操作, 就不再指定端口了. 让系统自动分配一个空闲端口.
socket = new DatagramSocket();
}
// 让这个客户端反复的从控制台读取用户输入的内容. 把这个内容构造成 UDP 请求, 发给服务器. 再读取服务器返回的 UDP 响应
// 最终再显示在客户端的屏幕上.
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("客户端启动!");
while (true) {
// 1. 从控制台读取用户输入的内容
System.out.print("-> "); // 命令提示符, 提示用户要输入字符串.
String request = scanner.next();
// 2. 构造请求对象, 并发给服务器.
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIp), serverPort);
socket.send(requestPacket);
// 3. 读取服务器的响应, 并解析出响应内容.
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
// 4. 显示到屏幕上.
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
// UdpEchoClient client = new UdpEchoClient("42.192.83.143", 9090);
client.start();
}
}
注意:
①
②
③
2.5 UDP翻译服务器
用继承编写:
package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpDictServer extends UdpEchoServer {
private Map<String, String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("cat", "小猫");
dict.put("dog", "小狗");
dict.put("fuck", "卧槽");
// 可以在这里继续添加千千万万个单词. 使每个单词都有一个对应的翻译.
}
// 是要复用之前的代码, 但是又要做出调整.
@Override
public String process(String request) {
// 把请求对应单词的翻译, 给返回回去.
return dict.getOrDefault(request, "该词没有查询到!");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9090);
// start 不需要重新再写一遍了. 直接就复用了之前的 start !
server.start();
}
}
如果在这里,不加@Override,万一方法名字/参数类型/参数个数/访问权限 搞错了,就无法构成重写了.
3.实现TCP版本的客户端和服务器
UDP提供的api有两个核心的类:ServerSocket和Socket
3.1 ServerSocket
ServerSocket 给服务器使用的.
(构造方法)方法签名 | 方法说明 |
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
3.2 Socket
Socket 既会给服务器使用,也会给客户端使用.
(构造方法)方法签名 | 方法说明 |
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的 进程建立连接 |
一个TCP数据报,就是一个字节数组byte[ ].
字节流,一个字节一个字节进行传输.
3.3 TCP回显服务器(始)
进入循环之后,要做的事情不是读取客户端的请求,而是先处理客户端的"连接".
一个服务器,要对应很多客户端.服务器内核里有很多客户端的连接,在应用程序中,这些连接是一个一个处理的.
内核中的"连接"就像一个一个"待办事项",这些待办事项在一个队列中,应用程序就需要一个一个完成这些任务.
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
// 这个操作就会绑定端口号
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
// 启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 这个写法, 是能自动关闭, 也行. 实现 Closeable 接口就可以这么写.
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
}
}
// 通过这个方法来处理一个连接的逻辑.
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
// 接下来就可以读取请求, 根据请求计算响应, 返回响应三步走了.
// Socket 对象内部包含了两个字节流对象, 可以把这俩字节流对象获取到, 完成后续的读写工作
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
// 一次连接中, 可能会涉及到多次请求/响应
while (true) {
// 1. 读取请求并解析. 为了读取方便, 直接使用 Scanner.
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()) {
// 读取完毕, 客户端下线.
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
// 这个代码暗含一个约定, 客户端发过来的请求, 得是文本数据, 同时, 还得带有空白符作为分割. (比如换行这种)
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回给客户端. 把 OutputStream 使用 PrinterWriter 包裹一下, 方便进行发数据.
PrintWriter writer = new PrintWriter(outputStream);
// 使用 PrintWriter 的 println 方法, 把响应返回给客户端.
// 此处用 println, 而不是 print 就是为了在结尾加上 \n . 方便客户端读取响应, 使用 scanner.next 读取.
writer.println(response);
// 这里还需要加一个 "刷新缓冲区" 操作.
writer.flush();
// 日志, 打印当前的请求详情.
System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(),
request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 在 finally 中加上 close 操作, 确保当前 socket 被及时关闭!!
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
注意:
①
②
③
④
⑤
⑥
问:
①
在这个程序中,涉及到两类Socket
1.ServerSocket(只有一个,生命周期跟随程序,不关闭也没事)
2.Socket
每次连接都会创建Socket,为了避免文件资源泄露,要确保能被关闭.
②
在前面代码中,对于Scanner和PrintWriter 没有进行close,会有文件资源泄露嘛?不会!!
3.4 TCP客户端
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
// 要和服务器通信, 就需要先知道, 服务器所在的位置.
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
// 这个 new 操作完成之后, 就完成了 tcp 连接的建立.
socket = new Socket(serverIp, serverPort);
}
public void start() {
System.out.println("客户端启动");
Scanner scannerConsole = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while (true) {
// 1. 从控制台输入字符串.
System.out.print("-> ");
String request = scannerConsole.next();
// 2. 把请求发送给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
// 使用 println 带上换行. 后续服务器读取请求, 就可以使用 scanner.next 来获取了
printWriter.println(request);
// 不要忘记 flush, 确保数据是真的发送出去了!!
printWriter.flush();
// 3. 从服务器读取响应.
Scanner scannerNetwork = new Scanner(inputStream);
String response = scannerNetwork.next();
// 4. 把响应打印出来
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
注意:
①
3.5 TCP回显服务器(修)
当前代码还存在一个很大的问题:
无法启动两个客户端!
①
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
// 此处不应该创建固定线程数目的线程池.
private ExecutorService service = Executors.newCachedThreadPool();
// 这个操作就会绑定端口号
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
// 启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 这个写法, 是能自动关闭, 也行. 实现 Closeable 接口就可以这么写.
// 这么写会有其他问题. (结合后面讲第二个问题, 再说这个事)
Socket clientSocket = serverSocket.accept();
// 单个线程, 不太方便完成这里的一边拉客, 一边介绍. 就需要多搞线程. 主线程专门负责拉客. 每次有一个客户端, 都创建一个新的线程去服务
Thread t = new Thread(() -> {
processConnection(clientSocket);
});
t.start();
}
}
// 通过这个方法来处理一个连接的逻辑.
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
// 接下来就可以读取请求, 根据请求计算响应, 返回响应三步走了.
// Socket 对象内部包含了两个字节流对象, 可以把这俩字节流对象获取到, 完成后续的读写工作
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
// 一次连接中, 可能会涉及到多次请求/响应
while (true) {
// 1. 读取请求并解析. 为了读取方便, 直接使用 Scanner.
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()) {
// 读取完毕, 客户端下线.
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
// 这个代码暗含一个约定, 客户端发过来的请求, 得是文本数据, 同时, 还得带有空白符作为分割. (比如换行这种)
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回给客户端. 把 OutputStream 使用 PrinterWriter 包裹一下, 方便进行发数据.
PrintWriter writer = new PrintWriter(outputStream);
// 使用 PrintWriter 的 println 方法, 把响应返回给客户端.
// 此处用 println, 而不是 print 就是为了在结尾加上 \n . 方便客户端读取响应, 使用 scanner.next 读取.
writer.println(response);
// 这里还需要加一个 "刷新缓冲区" 操作.
writer.flush();
// 日志, 打印当前的请求详情.
System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(),
request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 在 finally 中加上 close 操作, 确保当前 socket 被及时关闭!!
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
如果每个客户端只能发一次请求,发完就断开,上诉情况能得到一定缓解,但还是有类似的问题的.
处理多个消息,自然会延长proceConnection的执行时间,就让问题更严重了......
有一个连接,就有一个新的线程.
如果很多客户端, 频繁的连接断开,服务器就涉及到频繁创建/释放线程了.
所以这里可以使用多线程:
②
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
// 此处不应该创建固定线程数目的线程池.
private ExecutorService service = Executors.newCachedThreadPool();
// 这个操作就会绑定端口号
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
// 启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 这个写法, 是能自动关闭, 也行. 实现 Closeable 接口就可以这么写.
// 这么写会有其他问题. (结合后面讲第二个问题, 再说这个事)
Socket clientSocket = serverSocket.accept();
// 使用线程池, 来解决上述问题
service.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
// 单个线程, 不太方便完成这里的一边拉客, 一边介绍. 就需要多搞线程. 主线程专门负责拉客. 每次有一个客户端, 都创建一个新的线程去服务
// Thread t = new Thread(() -> {
// processConnection(clientSocket);
// });
// t.start();
}
}
// 通过这个方法来处理一个连接的逻辑.
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
// 接下来就可以读取请求, 根据请求计算响应, 返回响应三步走了.
// Socket 对象内部包含了两个字节流对象, 可以把这俩字节流对象获取到, 完成后续的读写工作
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
// 一次连接中, 可能会涉及到多次请求/响应
while (true) {
// 1. 读取请求并解析. 为了读取方便, 直接使用 Scanner.
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()) {
// 读取完毕, 客户端下线.
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
// 这个代码暗含一个约定, 客户端发过来的请求, 得是文本数据, 同时, 还得带有空白符作为分割. (比如换行这种)
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回给客户端. 把 OutputStream 使用 PrinterWriter 包裹一下, 方便进行发数据.
PrintWriter writer = new PrintWriter(outputStream);
// 使用 PrintWriter 的 println 方法, 把响应返回给客户端.
// 此处用 println, 而不是 print 就是为了在结尾加上 \n . 方便客户端读取响应, 使用 scanner.next 读取.
writer.println(response);
// 这里还需要加一个 "刷新缓冲区" 操作.
writer.flush();
// 日志, 打印当前的请求详情.
System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(),
request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 在 finally 中加上 close 操作, 确保当前 socket 被及时关闭!!
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
IO多路复用:一个线程完成多组IO操作
举个例子解释:
当三个人想吃三种不同的饭,如何买饭?
①A一个人买三种饭,买完一种去下一个摊位.
可以完成工作,但消耗的时间长,通过一个线程串行处理所有客户端的请求.
②A,B,C三个人同时去.
效率提高了,但引入了更多的线程,消耗了更多的系统资源.
③A一个人买三种饭,A点完一个去下一个,中间不停游走,哪个好了就把哪个拿去.
只有一个线程,但效率不必多线程方式低,同时系统开销还小