目录
2.实现UDP版本的回显服务器-客户端(ehco sever)
3.实现TCP版本的回显服务器-客户端(ehco sever)
1.网络编程:通过代码实现两个/多个进程之间实现通过网络来相互通信
2.客户端(client)/服务器(sever):客户端指主动发送网络数据的一方,服务器指被动接收网络数据的一方(处理客户端需求)
3.请求(request)/响应(response):请求指客户端给服务器发送数据,响应指服务器返回数据给客户端
4.客户端与服务器的交互方式
(1)一问一答:客户端给服务器发一个请求,服务器回应一个请求(比如浏览网页)
(2)多问一答:客户端发送多个请求,服务器返回一个响应(比如上传文件)
(3)一问多答:客户端发送一个请求,服务器返回多个响应(比如下载文件)
(4)多问多答:客户端发送多个请求,服务器返回多个响应(比如游戏串流)
我们谈到网络首先想到的自然是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.客户端解析响应并显示给用户
大部分的客户端—服务器都满足上述流程 ,如果不清楚我们可以看到下图
前面我们已经提到过TCP协议是面向字节流的,所以这里我们会使用到之前我们所学过的文件操作的内容。我们先介绍一些基础知识。
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造方法:
| 方法签名 | 方法说明 |
| ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket 方法:
| 方法签名 | 方法说明 |
| Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket 对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
| void close() | 关闭此套接字 |
Socket是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端 Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket 构造方法:
| 方法签名 | 方法说明 |
| Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的 进程建立连接 |
Socket 方法:
| 方法签名 | 方法说明 |
| InetAddress getInetAddress() | 返回套接字所连接的地址 |
| InputStream getInputStream() | 返回此套接字的输入流 |
| OutputStream getOutputStream() | 返回此套接字的输出流 |
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
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.分布式服务器(增加运算资源)