• Java网络编程


    目录

    1.一些基础概念

     2.实现UDP版本的回显服务器-客户端(ehco sever)

     3.实现TCP版本的回显服务器-客户端(ehco sever)

    3.1 ServerSocket API 

    3.2 Socket API 

    3.3 TCP中的长短连接


    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): 

    1. import java.io.IOException;
    2. import java.net.DatagramPacket;
    3. import java.net.DatagramSocket;
    4. public class UdpEchoServer {
    5. // 要想创建 UDP 服务器, 首先要先打开一个 socket 文件.
    6. private DatagramSocket socket=null;
    7. public UdpEchoServer(int port) throws IOException{
    8. socket=new DatagramSocket(port);
    9. }
    10. public void start()throws IOException{
    11. System.out.println("服务器启动");
    12. while(true){
    13. // 1. 读取客户端发来的请求
    14. DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
    15. socket.receive(requestPacket);
    16. // 2. 对请求进行解析, 把 DatagramPacket 转成一个 String
    17. String request=new String(requestPacket.getData(),0,requestPacket.getLength());
    18. // 3. 根据请求, 处理响应
    19. String response=process(request);
    20. // 4. 把响应构造成 DatagramPacket 对象.
    21. // 构造响应对象, 要搞清楚, 对象要发给谁!! 谁发的请求, 就把响应发给谁
    22. DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,
    23. requestPacket.getSocketAddress());//response.getBytes().length获取字节数长度
    24. // 5. 把这个 DatagramPacket 对象返回给客户端.
    25. socket.send(responsePacket);
    26. System.out.printf("[%s:%d] req=%s; resp=%s\n", requestPacket.getAddress().toString(), requestPacket.getPort(),
    27. request, response);
    28. }
    29. }
    30. public String process(String req){
    31. return req;
    32. }
    33. public static void main(String[] args) throws IOException {
    34. UdpEchoServer udpEchoServer=new UdpEchoServer(8000);
    35. udpEchoServer.start();
    36. }
    37. }

    客户端(Client):

    1. import java.io.IOException;
    2. import java.net.DatagramPacket;
    3. import java.net.DatagramSocket;
    4. import java.net.InetAddress;
    5. import java.net.UnknownHostException;
    6. import java.util.Scanner;
    7. public class UdpEchoClient {
    8. private DatagramSocket socket=null;
    9. public UdpEchoClient()throws IOException{
    10. // 客户端的端口号, 一般都是由操作系统自动分配的. 虽然手动指定也行, 习惯上还是自动分配比较好
    11. socket=new DatagramSocket();
    12. }
    13. public void start() throws IOException {
    14. Scanner scanner = new Scanner(System.in);
    15. while(true){
    16. // 1. 让客户端从控制台读取一个请求数据.
    17. System.out.print("> ");
    18. String request = scanner.next();
    19. // 2. 把这个字符串请求发送给服务器. 构造 DatagramPacket
    20. // 构造的 Packet 既要包含 要传输的数据, 又要包含把数据发到哪里
    21. DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
    22. InetAddress.getByName("127.0.0.1"),8000);
    23. // 3. 把数据报发给服务器
    24. socket.send(requestPacket);
    25. // 4. 从服务器读取响应数据
    26. DatagramPacket responsePacket =new DatagramPacket(new byte[4096],4096);
    27. socket.receive(responsePacket);
    28. // 5. 把响应数据获取出来, 转成字符串.
    29. String response=new String(responsePacket.getData(),0,responsePacket.getLength());
    30. System.out.printf("req: %s; resp: %s\n", request, response);
    31. }
    32. }
    33. public static void main(String[] args) throws IOException {
    34. UdpEchoClient udpEchoClient=new UdpEchoClient();
    35. udpEchoClient.start();
    36. }
    37. }

    客户端—服务器的工作流程:

    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发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接

    短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据

    长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据

    对比以上长短连接,两者区别如下:

    建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要 第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时 的,长连接效率更高。

    主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送 请求,也可以是服务端主动发。

    两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。 

    我们以下以长连接示例:

    1. import javax.swing.*;
    2. import java.io.IOException;
    3. import java.io.InputStream;
    4. import java.io.OutputStream;
    5. import java.io.PrintWriter;
    6. import java.net.ServerSocket;
    7. import java.net.Socket;
    8. import java.util.Scanner;
    9. import java.util.concurrent.ExecutorService;
    10. import java.util.concurrent.Executors;
    11. public class TcpEchoServer {
    12. private ServerSocket serverSocket=null;
    13. public TcpEchoServer(int port) throws IOException{
    14. serverSocket=new ServerSocket(port);
    15. }
    16. public void start() throws IOException{
    17. System.out.println("服务器启动!");
    18. ExecutorService service= Executors.newCachedThreadPool();
    19. while(true){
    20. // 如果当前没有客户端来建立连接, 就会阻塞等待!!
    21. Socket clientSocket=serverSocket.accept();
    22. // [版本1] 单线程版本, 存在 bug, 无法处理多个客户端
    23. //processConnect(clientSocket);
    24. // [版本2] 多线程版本. 主线程负责拉客, 新线程负责通信
    25. //涉及到频繁创建销毁线程, 在高并发的情况下, 负担比较重的
    26. // Thread t=new Thread(()->{
    27. // try {
    28. // processConnect(clientSocket);
    29. // } catch (IOException e) {
    30. // e.printStackTrace();
    31. // }
    32. // });
    33. // t.start();
    34. // [版本3] 使用线程池, 来解决频繁创建销毁线程的问题
    35. service.submit(new Runnable() {
    36. @Override
    37. public void run() {
    38. try {
    39. processConnect(clientSocket);
    40. } catch (IOException e) {
    41. e.printStackTrace();
    42. }
    43. }
    44. });
    45. }
    46. }
    47. // 一个连接过来了, 服务方式可能有两种:
    48. // 1. 一个连接只进行一次数据交互 (一个请求+一个响应) 短连接
    49. // 2. 一个连接进行多次数据交互 (N 个请求 + N 个响应) 长连接
    50. // 此处来写长连接的版本
    51. public void processConnect(Socket clientSocket) throws IOException{
    52. System.out.printf("[%s:%d] 建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
    53. try(InputStream inputStream=clientSocket.getInputStream();
    54. OutputStream outputStream=clientSocket.getOutputStream()){
    55. Scanner scanner=new Scanner(inputStream);
    56. PrintWriter printWriter=new PrintWriter(outputStream);
    57. // 这里是长连接的写法, 需要通过 循环 来获取到多次交互情况.
    58. while (true){
    59. if(!scanner.hasNext()){
    60. // 连接断开. 当客户端断开连接的时候, 此时 hasNext 就会返回 false
    61. System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
    62. break;
    63. }
    64. // 1. 读取请求并解析
    65. String request= scanner.next();
    66. // 2. 根据请求计算响应
    67. String response=process(request);
    68. // 3. 把响应写回给客户端
    69. printWriter.println(response);
    70. //刷新缓冲区, 避免数据没有真的发出去
    71. printWriter.flush();
    72. System.out.printf("[%s:%d] req: %s; resp: %s\n",
    73. clientSocket.getInetAddress().toString(), clientSocket.getPort(),
    74. request, response);
    75. }
    76. }finally {
    77. clientSocket.close();
    78. //客户端断开后关闭连接
    79. }
    80. }
    81. public String process(String req) {
    82. return req;
    83. }
    84. public static void main(String[] args) throws IOException{
    85. TcpEchoServer tcpEchoServer=new TcpEchoServer(8000);
    86. tcpEchoServer.start();
    87. }
    88. }

     

    1. import java.io.IOException;
    2. import java.io.InputStream;
    3. import java.io.OutputStream;
    4. import java.io.PrintWriter;
    5. import java.net.Socket;
    6. import java.util.Scanner;
    7. public class TcpEchoClient {
    8. private Socket socket=null;
    9. public TcpEchoClient() throws IOException {
    10. // new 这个对象, 需要和服务器建立连接的!!
    11. // 建立连接, 就得知道服务器在哪里!!
    12. socket=new Socket("127.0.0.1",8000);
    13. }
    14. public void start() throws IOException{
    15. // 由于实现的是长连接, 一个连接会处理 N 个请求和响应
    16. Scanner scanner = new Scanner(System.in);
    17. try(InputStream inputStream=socket.getInputStream();
    18. OutputStream outputStream=socket.getOutputStream()){
    19. Scanner scannerNet=new Scanner(inputStream);
    20. PrintWriter printWriter=new PrintWriter(outputStream);
    21. while (true){
    22. // 1. 从控制台读取用户的输入.
    23. System.out.print("> ");
    24. String request = scanner.next();
    25. // 2. 把请求发送给服务器
    26. //使用println而不是write是为了加上\n
    27. /*
    28. 当服务器端输出流使用writer(String x)方法时,
    29. 客户端使用Scanner类的hasNextLine()方法和nextLine()方法从输入流中读取数据时,
    30. 由于nextLine()方法无法读取到行分隔符,该方法将造成阻塞,客户端将不会显示服务器端发来的信息,
    31. 解决方法:当使用write(String x)时,在字符串后面加上行分隔符“\r\n”,或者使用println(Stirng x)方法
    32. */
    33. printWriter.println(request);
    34. printWriter.flush();
    35. // 3. 从服务器读取响应
    36. String response=scannerNet.next();
    37. // 4. 把结果显示到界面上
    38. System.out.printf("req: %s; resp: %s\n", request, response);
    39. }
    40. }
    41. }
    42. public static void main(String[] args) throws IOException {
    43. TcpEchoClient client = new TcpEchoClient();
    44. client.start();
    45. }
    46. }
    1. import java.io.IOException;
    2. import java.net.ServerSocket;
    3. import java.util.HashMap;
    4. import java.util.Map;
    5. public class TcpDictServer extends TcpEchoServer{
    6. private Map dict = new HashMap<>();
    7. public TcpDictServer(int port) throws IOException {
    8. super(port);
    9. dict.put("cat", "小猫");
    10. dict.put("dog", "小狗");
    11. }
    12. public String process(String req) {
    13. return dict.getOrDefault(req, "查无此词");
    14. }
    15. public static void main(String[] args) throws IOException {
    16. TcpDictServer server = new TcpDictServer(8000);
    17. server.start();
    18. }
    19. }

    有一点我们需要注意的是Server服务器端需要在客户端退出后及时关闭连接,防止资源占用造成更严重的后果。这里我们使用了线程池来缓解频繁销毁创建线程的问题,但是这在大型服务器上是远远不够的,此时我们通常会采用以下三种方式来解决高并发的问题(了解即可)。

    1.采用协程

    2.IO多路复用

    3.分布式服务器(增加运算资源)

  • 相关阅读:
    《Java 核心技术卷1 基础知识》第三章 Java 的基本程序设计结构 笔记
    2011年03月17日 Go生态洞察:探索Go与C的交互——Cgo
    springmvc总结
    正则表达式练习
    Harmony mac在DevEco-Studio终端中输入hdc shell命令报错,找不到文件或路径处理方式
    BootStrap中的布局
    星闪技术 NearLink 一种专门用于短距离数据传输的新型无线通信技术
    外汇天眼:CFTC处罚Advantage Futures 39.5万美元
    计算机毕业设计 基于SpringBoot的驾校管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解
    传感器工作原理
  • 原文地址:https://blog.csdn.net/weixin_60778429/article/details/126186933