TCP 和 UDP 的区别
在传输层,TCP 协议是有连接的,可靠传输,面向字节流,全双工。 而 UDP 协议是无连接的,不可靠传输,面向数据报,全双工。
有连接和无连接的区别是在进行网络通信的时候,通信双方有没有保存对端的地址信息。即假设 A 和 B 进行通信,A 保存了 B 的地址信息,B 也保存了 A 的地址信息,此时双方都知道和谁建立了连接,这就是有连接的通信。在之前的 UDP 数据报套接字编程中就提到过 UDP 是无连接的,所以在发送数据报的时候要加上对端的信息,防止丢包。
可靠传输是通过各种手段来防止丢包的出现,而不可靠传输则没有做任何处理直接把数据报传输过去。但是可靠传输不意味着能 100% 把数据报完整无误地传输给对方,只是尽可能降低丢包发生的概率,并且可靠传输是要使用很多手段来保持的,所以付出的代价相比于不可靠传输要大。
面向字节流就是以字节为单位来进行数据的传输,面向数据报就是以数据报为单位进行数据的传输。
全双工就是通信的双发可以同时给对方发送数据,但是半双工是指双方只有一方可以发送数据。
TCP 流套接字 API 介绍
ServerSocket
ServerSocket 是 TCP 服务端 Socket 的 API。
构造方法:
| 方法名 | 说明 |
|---|---|
| ServerSocket(int port) | 创建一个 TCP 服务端流套接字 Socket,并绑定端口号 |
ServerSocket 方法:
| 方法名 | 返回值 | 说明 |
|---|---|---|
| accept() | Socket | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端 Socket 对象,并基于该 Socket 建立与客户端的连接,否则阻塞等待 |
| close() | void | 关闭此套接字 |
Socket
Socket 是客户端 Socket 或者是服务端那边收到客户端建立连接的请求(通过 accept() 方法)返回的 Socket 对象。
不管是客户端还是服务端的 Socket 对象,他们都保留了对端的地址信息,这也是 TCP 协议有连接的体现。
Socket 构造方法:
| 方法名 | 说明 |
|---|---|
| Socket(String host, int port) | 创建一个客户端流套接字 Socket,并于对应 IP 的主机对应的端口的进程建立连接 |
Socket 方法:
| 方法名 | 返回值 | 说明 |
|---|---|---|
| getInetAddress() | InetAddress | 返回套接字所连接的地址 |
| getInputStream() | InputStream | 返回此套接字的输入流 |
| getOutputStream() | OutputStream | 返回此套接字的输出流 |
回显服务器
首先在回显服务器的构造方法里初始化我们的 ServerSocket。
public class TcpEchoServer {
private ServerSocket serverSocket;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动...");
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
// 与客户端建立连接
Socket clientSocket = serverSocket.accept();
// 处理客户端发出的多个请求
executorService.submit(() -> {
try {
processClient(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
private void processClient(Socket clientSocket) throws IOException {
// 获取输入流和输出流
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress(), clientSocket.getPort());
// 对输入流和输出流进行进一步的封装,方便我们的使用
Scanner scanner = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true) {
if (!scanner.hasNext()) {
System.out.printf("[%s:%d] 客户端下线\n", clientSocket.getInetAddress(), clientSocket.getPort());
break;
}
// 解析请求
String request = scanner.next();
// 计算响应
String response = process(request);
// 发送响应
writer.println(response);
// 因为此时的响应数据还在缓存区里,所以需要使用 flush 来将内存的数据发送出去
writer.flush();
System.out.printf("[%s:%d] request:%s response:%s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
// 当请求处理完的时候记得关闭服务器与客户端的连接,防止资源泄漏
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
客户端
首先在客户端构造方法建立与服务器的连接:
public class TcpEchoClient {
private Socket socket;
public TcpEchoClient(String serverIP, int port) throws IOException {
// 与服务器建立连接
socket = new Socket(serverIP, port);
}
public void start() {
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
// 对输入流和输出流进行进一步的封装
Scanner scanner = new Scanner(System.in);
Scanner scanner2 = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true) {
// 发送多个请求和接收多个响应
if (!scanner.hasNext()) {
break;
}
// 发送请求
String request = scanner.next();
writer.println(request);
writer.flush();
// 接收响应
String response = scanner2.next();
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
细节说明
在我们使用 PrintWriter 的 writer.println(xxx) 之后,我们的数据其实还保留在缓存区中,也就是还没发出去,我们需要通过 flush() 方法来刷新缓存区的数据,才能将数据真正发送到对端去。
我们不可以使用 writer.print 这种没有自动添加换行符的方法,因为我们在接收数据的时候,使用的是 Scanner 的 next() 方法,next() 是要接收到空白符(包括换行符,制表符,翻页符…)才停止接收的。如果你使用 print 来发送数据,这时候的数据是没有带任何空白符的,那么就不会停止接收数据而是继续等待空白符的到来,这时候服务器就无法处理客户端的请求。
你在客户端的控制台输入的回车不算进数据的换行符里,控制台输入的回车时,只是将数据交给了客户端程序,并不会自动将这些数据转换为网络流中的换行符。 换一句话说,控制台的回车只是结束你在控制台的输入,并不会自动在数据末尾加上换行符。


