目录
2.实现UDP版本的回显服务器-客户端(ehco sever)
3.实现TCP版本的回显服务器-客户端(ehco sever)
1.一些基础概念
1.网络编程:通过代码实现两个/多个进程之间实现通过网络来相互通信
2.客户端(client)/服务器(sever):客户端指主动发送网络数据的一方,服务器指被动接收网络数据的一方(处理客户端需求)
3.请求(request)/响应(response):请求指客户端给服务器发送数据,响应指服务器返回数据给客户端
4.客户端与服务器的交互方式
(1)一问一答:客户端给服务器发一个请求,服务器回应一个请求(比如浏览网页)
(2)多问一答:客户端发送多个请求,服务器返回一个响应(比如上传文件)
(3)一问多答:客户端发送一个请求,服务器返回多个响应(比如下载文件)
(4)多问多答:客户端发送多个请求,服务器返回多个响应(比如游戏串流)
2.实现UDP版本的回显服务器-客户端(ehco sever)
我们谈到网络首先想到的自然是TCP或者UDP传输层协议,那么我们首先来看一下他们的区别在哪。我们先简单概括一下。
TCP:有连接,可靠传输,面向字节流,全双工
UDP:无连接,不可靠传输,面向数据报,全双工
我们再来逐个解释这些词是什么意思
有连接:类似打电话,先建立连接,然后通信
无连接:类似发微信,不必建立连接,直接通信
可靠传输:数据对方有没有接收到发送方能够有感知
不可靠传输:数据对方有没有接收到发送方能够无感知
注意:即使是可靠传输,在网络通信的过程中也无法保证100%送达
全双工:双向通信,能A->B,B->A同时进行
半双工:单向通信,要么A->B,要么B->A
那么什么是回显客户端——服务器呢?其实就是客户端发送什么服务器就返回什么,我们直接看到代码。
服务器(Sever):
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UdpEchoServer {
// 要想创建 UDP 服务器, 首先要先打开一个 socket 文件.
private DatagramSocket socket=null;
public UdpEchoServer(int port) throws IOException{
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);
// 2. 对请求进行解析, 把 DatagramPacket 转成一个 String
String request=new String(requestPacket.getData(),0,requestPacket.getLength());
// 3. 根据请求, 处理响应
String response=process(request);
// 4. 把响应构造成 DatagramPacket 对象.
// 构造响应对象, 要搞清楚, 对象要发给谁!! 谁发的请求, 就把响应发给谁
DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());//response.getBytes().length获取字节数长度
// 5. 把这个 DatagramPacket 对象返回给客户端.
socket.send(responsePacket);
System.out.printf("[%s:%d] req=%s; resp=%s\n", requestPacket.getAddress().toString(), requestPacket.getPort(),
request, response);
}
}
public String process(String req){
return req;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer=new UdpEchoServer(8000);
udpEchoServer.start();
}
}
客户端(Client):
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket=null;
public UdpEchoClient()throws IOException{
// 客户端的端口号, 一般都是由操作系统自动分配的. 虽然手动指定也行, 习惯上还是自动分配比较好
socket=new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while(true){
// 1. 让客户端从控制台读取一个请求数据.
System.out.print("> ");
String request = scanner.next();
// 2. 把这个字符串请求发送给服务器. 构造 DatagramPacket
// 构造的 Packet 既要包含 要传输的数据, 又要包含把数据发到哪里
DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName("127.0.0.1"),8000);
// 3. 把数据报发给服务器
socket.send(requestPacket);
// 4. 从服务器读取响应数据
DatagramPacket responsePacket =new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
// 5. 把响应数据获取出来, 转成字符串.
String response=new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.printf("req: %s; resp: %s\n", request, response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient udpEchoClient=new UdpEchoClient();
udpEchoClient.start();
}
}
客户端—服务器的工作流程:
1.客户端根据用户输入,构造请求
2.客户端发送请求给服务器
3.服务器读取并解析请求
4.服务器根据请求计算响应(服务器核心逻辑)
5.服务器构造响应数据并返回给客户端
6.客户端读服务器的响应
7.客户端解析响应并显示给用户
大部分的客户端—服务器都满足上述流程 ,如果不清楚我们可以看到下图
3.实现TCP版本的回显服务器-客户端(ehco sever)
前面我们已经提到过TCP协议是面向字节流的,所以这里我们会使用到之前我们所学过的文件操作的内容。我们先介绍一些基础知识。
3.1 ServerSocket API
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造方法:
方法签名 | 方法说明 |
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket 方法:
方法签名 | 方法说明 |
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket 对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
3.2 Socket API
Socket是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端 Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket 构造方法:
方法签名 | 方法说明 |
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的 进程建立连接 |
Socket 方法:
方法签名 | 方法说明 |
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
3.3 TCP中的长短连接
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。
对比以上长短连接,两者区别如下:
建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要 第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时 的,长连接效率更高。
主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送 请求,也可以是服务端主动发。
两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
我们以下以长连接示例:
import javax.swing.*;
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("服务器启动!");
ExecutorService service= Executors.newCachedThreadPool();
while(true){
// 如果当前没有客户端来建立连接, 就会阻塞等待!!
Socket clientSocket=serverSocket.accept();
// [版本1] 单线程版本, 存在 bug, 无法处理多个客户端
//processConnect(clientSocket);
// [版本2] 多线程版本. 主线程负责拉客, 新线程负责通信
//涉及到频繁创建销毁线程, 在高并发的情况下, 负担比较重的
// Thread t=new Thread(()->{
// try {
// processConnect(clientSocket);
// } catch (IOException e) {
// e.printStackTrace();
// }
// });
// t.start();
// [版本3] 使用线程池, 来解决频繁创建销毁线程的问题
service.submit(new Runnable() {
@Override
public void run() {
try {
processConnect(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
// 一个连接过来了, 服务方式可能有两种:
// 1. 一个连接只进行一次数据交互 (一个请求+一个响应) 短连接
// 2. 一个连接进行多次数据交互 (N 个请求 + N 个响应) 长连接
// 此处来写长连接的版本
public void processConnect(Socket clientSocket) throws IOException{
System.out.printf("[%s:%d] 建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()){
Scanner scanner=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
// 这里是长连接的写法, 需要通过 循环 来获取到多次交互情况.
while (true){
if(!scanner.hasNext()){
// 连接断开. 当客户端断开连接的时候, 此时 hasNext 就会返回 false
System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
// 1. 读取请求并解析
String request= scanner.next();
// 2. 根据请求计算响应
String response=process(request);
// 3. 把响应写回给客户端
printWriter.println(response);
//刷新缓冲区, 避免数据没有真的发出去
printWriter.flush();
System.out.printf("[%s:%d] req: %s; resp: %s\n",
clientSocket.getInetAddress().toString(), clientSocket.getPort(),
request, response);
}
}finally {
clientSocket.close();
//客户端断开后关闭连接
}
}
public String process(String req) {
return req;
}
public static void main(String[] args) throws IOException{
TcpEchoServer tcpEchoServer=new TcpEchoServer(8000);
tcpEchoServer.start();
}
}
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() throws IOException {
// new 这个对象, 需要和服务器建立连接的!!
// 建立连接, 就得知道服务器在哪里!!
socket=new Socket("127.0.0.1",8000);
}
public void start() throws IOException{
// 由于实现的是长连接, 一个连接会处理 N 个请求和响应
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()){
Scanner scannerNet=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
while (true){
// 1. 从控制台读取用户的输入.
System.out.print("> ");
String request = scanner.next();
// 2. 把请求发送给服务器
//使用println而不是write是为了加上\n
/*
当服务器端输出流使用writer(String x)方法时,
客户端使用Scanner类的hasNextLine()方法和nextLine()方法从输入流中读取数据时,
由于nextLine()方法无法读取到行分隔符,该方法将造成阻塞,客户端将不会显示服务器端发来的信息,
解决方法:当使用write(String x)时,在字符串后面加上行分隔符“\r\n”,或者使用println(Stirng x)方法
*/
printWriter.println(request);
printWriter.flush();
// 3. 从服务器读取响应
String response=scannerNet.next();
// 4. 把结果显示到界面上
System.out.printf("req: %s; resp: %s\n", request, response);
}
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient();
client.start();
}
}
import java.io.IOException;
import java.net.ServerSocket;
import java.util.HashMap;
import java.util.Map;
public class TcpDictServer extends TcpEchoServer{
private Map<String, String> dict = new HashMap<>();
public TcpDictServer(int port) throws IOException {
super(port);
dict.put("cat", "小猫");
dict.put("dog", "小狗");
}
public String process(String req) {
return dict.getOrDefault(req, "查无此词");
}
public static void main(String[] args) throws IOException {
TcpDictServer server = new TcpDictServer(8000);
server.start();
}
}
有一点我们需要注意的是Server服务器端需要在客户端退出后及时关闭连接,防止资源占用造成更严重的后果。这里我们使用了线程池来缓解频繁销毁创建线程的问题,但是这在大型服务器上是远远不够的,此时我们通常会采用以下三种方式来解决高并发的问题(了解即可)。
1.采用协程
2.IO多路复用
3.分布式服务器(增加运算资源)