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

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.分布式服务器(增加运算资源)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值