【Java成王之路】EE初阶第十篇:(网络编程) 4

上节回顾

套接字,TCP版本的套接字 API

ServerSocket 服务器这边使用的
Socket: 服务器和客户端都需要使用

对于服务器来说:

1.创建ServerSocket 关联上一个端口号   (称为listenSocket)

2.调用ServerSocket 的 accept 方法

accept的功能是把一个内核建立好的连接给拿到代码中处理

accpept会返回一个Socket实例,称为clientSocket

3.使用clientSocket的getlnputStream和getOutputStream

得到字节流对象,就可以进行读取写入了

4.当客户端断开连接之后,服务器就应该要及时的关闭clientSocket.(否则可能会出现文件资源泄漏情况)

对于客户端:

1.创建一个Socket对象.创建的同时指定服务器的ip和端口(这个操作就会让客户端和服务器建立TCP连接.这个连接建立的过程就是传说中的"三次握手",这个流程是内核完成的,用户代码感知不到)

2.客户端就可以通过Socket对象的getlnputStream和getOutputStream来和服务器进行通信了

在上一篇的TCP代码中,其实还存在一个很严重的bug!!!

实际开发中,一个服务器应该要对应很多个客户端!!而且升值是成千上万个.

现在我们在IDEA上再启动一个客户端

 

现在这里就有两个客户端了

 当前就发现,当启动第二个客户端的时候,服务器就没有提示"上线"

 

 当在第二个客户端发送数据的时候,发现没有任何反应

当退出第一个客户端的时候,神奇的事情出现了!!!服务器提示了客户端2上线,也得到了hello2这样响应!!

总结:当前咱们的服务器同一时刻,只能给一个客户端提供服务,只有前一个客户端下了,下一个客户端才能上来.这样的设定,显然是不科学的.

原因是啥?

解决方案:

使用多线程!

主线程里面循环调用 accept  每次获取到一个连接,就创建一个线程,让这个线程来处理这个连接

 服务器代码改进

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;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: 灯泡和大白
 * Date: 2022-07-26
 * Time: 18:36
 */
//这个代码和前面普通的 TCP 回显服务器,基本差不多,只不过,这里面增加了多线程的处理
    //针对每个客户端都搞一个新的线程
public class TcpThreadEchoServer {
    private ServerSocket listenSocket = null;

    public TcpThreadEchoServer(int port) throws IOException {
        listenSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true) {
            // 在这个代码中, 通过创建线程, 就能保证 accept 调用完毕之后, 就能立刻再次调用 accept .
            Socket clientSocket = listenSocket.accept();
            // 创建一个线程来给这个客户端提供服务
            Thread t = new Thread() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            };
            t.start();
        }
    }

    // 这个代码和前面是一样的了.
    public void processConnection(Socket clientSocket) throws IOException {
        String log = String.format("[%s:%d] 客户端上线!", clientSocket.getInetAddress().toString(),
                clientSocket.getPort());
        System.out.println(log);
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            while (true) {
                // 1. 读取请求并解析
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNext()) {
                    log = String.format("[%s:%d] 客户端下线!", clientSocket.getInetAddress().toString(),
                            clientSocket.getPort());
                    System.out.println(log);
                    break;
                }
                String request = scanner.next();
                // 2. 根据请求计算响应
                String response = process(request);
                // 3. 把响应写回到客户端
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                printWriter.flush();

                log = String.format("[%s:%d] req: %s; resp: %s", clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(), request, response);
                System.out.println(log);
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            clientSocket.close();
        }
    }

    // 回显服务器, 直接把请求返回即可
    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpThreadEchoServer server = new TcpThreadEchoServer(9090);
        server.start();
    }
}

 实际开发中,客户端的数量可能会很多啊.

这个时候能行吗?

虽然线程比进程更轻量,但是如果有很多客户端连接又退出,这就会导致咱们当前的服务器里频繁的创建销毁线程.....

这个时候还是会有很大的成本的

如何改进这个问题?

线程池

线程池版本

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 TcpThreadPoolEchoServer {
    private ServerSocket listenSocket = null;

    public TcpThreadPoolEchoServer(int port) throws IOException {
        listenSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true) {
            Socket clientSocket = listenSocket.accept();
            // 使用线程池来处理当前的 processConnection
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    public void processConnection(Socket clientSocket) throws IOException {
        String log = String.format("[%s:%d] 客户端上线!", clientSocket.getInetAddress().toString(),
                clientSocket.getPort());
        System.out.println(log);
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            while (true) {
                // 1. 读取请求并解析
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNext()) {
                    log = String.format("[%s:%d] 客户端下线!", clientSocket.getInetAddress().toString(),
                            clientSocket.getPort());
                    System.out.println(log);
                    break;
                }
                String request = scanner.next();
                // 2. 根据请求计算响应
                String response = process(request);
                // 3. 把响应写回到客户端
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                printWriter.flush();

                log = String.format("[%s:%d] req: %s; resp: %s", clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(), request, response);
                System.out.println(log);
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            clientSocket.close();
        }
    }

    // 回显服务器, 直接把请求返回即可
    public String process(String request) {
        return request;
    }

}

假设极端情况下,一个服务器面临很多很多客户端,这些客户端,连上之后,并没有退出.

这个时候服务器这边同一时刻,就会存在很多很多线程(上万个线程)

这个情况,会有其他的问题嘛?这是科学的处理方法吗?

实际上,如果出现这样的情况,是不科学的!!!

每个线程都要占据一定的系统资源,如果线程太多太多,此时很多系统资源就会非常紧张,达到一定程度,及其可能就扛不住了(宕机了) 

系统资源指的是:内存资源,CPU资源

针对这种线程特别多的情况,如何改进呢?

1.可以使用协程来代替线程,完成并发.(很多协程的实现是 M:N) 协程比线程还轻量(GO语言)

2.可以使用IO多路复用的机制,完成并发.

IO多路复用:从根本上解决服务器处理高并发的问题.

在内核里来支持这样的一个功能

刚才假设有1万个客户端,在服务器这边会用一定的数据结构来把这1万个客户端对应的socket都存好.不需要一个线程对应一个客户端了,就一共只有一个/几个线程

IO多路复用机制,就能够做到,那个socket上面有数据了,就通知到应用程序,让这个线程来从这个socket中读数据.

3.使用多个主机(分布式)

提供更多的硬件资源

这三点都是一个基本的来处理一个高并发场景,所使用的的办法

网络通信的原理

网络协议是分层的.

应用层:

应用层协议,是程序猿打交道的最多的协议.

应用层是直接和程序相关的

1.使用现成的应用层协议来进行开发.

2.程序猿自己定制一个协议,完成需求

协议不是一成不变的.协议也不是遥不可及的.协议很多时候都是程序猿自己约定的.

只要客户端 + 服务器都是自己开发的,这个时候中间使用啥样的协议,完全是咱们自己说了算!!!

 现成的应用层协议:

其实也是有很多的.

用的最多的应用层协议HTTP协议.

比如,我在浏览器上,输入一个地址,然后浏览器打开了一个网页.(如果电脑没网,能打的开嘛)

这个过程就是,客户端(浏览器)给bing的服务器发送了一个请求,请求中就包含另一个链接.

然后bing的服务器给浏览器返回了一个响应,这个响应就是一个网页.

在这个网络通信中,使用的应用层协议,就是HTTP协议.(注意,当前很多网站,其实是使用了HTTPS,HTTPS也是基于HTTP).

再比如,手机端,打开了一个饿了么/美团外卖,先看到一个商家列表,点进去,就能看到吃的列表.还有下单,支付......这些过程客户端和服务器之间的通信,大概率也是在使用HTTP协议.

除了HTTP之外,还有很多其他的应用层协议

FTP.现在已经很少见了.以前的时候,文件传输,就可以使用

SSH:后面linux会涉及到.(使用一个终端软件(例如xahell),就可以连接到一个服务器)(在一个遥远的机房里面)

TELNET:现在不常见,如果进行一些嵌入式开发.....

DNS:域名解析的协议

........

应用层协议种类非常多.....

自定义协议,程序猿自己来约定.

约定,请求是啥格式.

响应是啥格式.

客户端和服务器之间就可以按照这样的约定来进行开发了.

(这个事在日常工作中经常会涉及) 

假设开发一个外卖软件.

客户端(APP):这一波程序猿开发客户端

服务器:这一波程序猿开发服务器

假设现在有一个需求:要求在外卖软件的首页,就能显示一个"优惠活动",用户参与活动,就能抽取红包.

客户端:就得修改界面,能够显示优惠活动的详情.

服务器:修改逻辑后,针对啥样的用户能参加优惠,具体咋样能领到红包,红包金额是多少...

这个时候客户端启动的时候,就要向服务器查询,当前是否能够参与活动.

服务器就要返回"是"/"否"

 这里就要求,客户端和服务器必须统一!!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

K稳重

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值