5.6.3 网络套接字章 TCP服务器客户端

1.0 ServerSocket 服务端 API

概念

  • ServerSocket 是创建TCP服务端Socket的API。
  • TCP是字节流,我们并不需要像UDP那样有个专门的数据报,换句话来说一个tcp数据报,就是一个字节数组

ServerSocket 构造方法

方法签名方法说明
ServerSocket(int port)创建一个服务端流套接字Socket,并绑定到指定端口

ServerSocket 方法

方法签 名方法说明
Socket accept()开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket 对象,并基于该Socket建立与客户端的连接,否则阻塞等待
Socket accept()关闭此套接字

2.0 Socket 客户端/服务端 API

概念

  • Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端 Socket。
  • 不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据 的。
  • Socket 既可以客户端用也可以服务器用

构造方法

方法签名方法说明
Socket(String host, int port)创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的 进程建立连接

Socket 方法

方法签名方法说明
InetAddress getInetAddress()返回套接字所连接的地址
InputStream getInputStream()返回此套接字的输入流
OutputStream getOutputStream()返回此套接字的输出流

3.0 短连接和长连接

TCP程序涉及到两种写法

  • 1.短连接一个链接中只能传输一次请求和响应
  • 2.长连接 一个连接中,可以传输多次请求和响应
  • 长连接与短连接在代码的区别就是有无while死循环,加了死循环,只要建立连接且不break,就可以一直通信

4.0 TCP的回显服务器

概念

  • 服务器启动后在while循环时,第一步不是接收客户端请求,而是与客户端连接
  • 不用握手,握手时系统内科负责的,写代码的过程是感受不到的
  • 连接是在握手之后的处理
  • 一次IO经历两个部分,1等(阻塞) 2拷贝数据

存在的问题与解决

  • Q1:ServerSocket中的队列是啥? 在系统内核眼里,ServerSocket这个对象也是一个文件,ServerSocket里有一个队列,当客户端和服务器尝试建立连接的时候,服务器就会和客户端建立连接时,服务器就会和客户端进行一系列数据交互,称为"握手"(这一步由操作系统完成),这个过程建立完了之后,连接就建立好了,ServerSocket队列中就会添加这些建立好连接的对象**(生产),要处理这些连接,就会取队列元素(消费)**,每个ServerSocket对象都有一个队列

  • Q2:程序中建立连接的过程 一个服务器,要对应很多的客户端,服务器内核里有很多客户端的连接(已经握手了),但是应用程序还是要一个一个的建立连接,内核的连接就像一个一个"待办事项",这些代办事项在一个队列的数据结构中,应用程序就要一个一个的完成这些任务,要完成任务就需要serverSocket.accept方法取任务,如果客户端没 有连接,那么accept就会阻塞

  • Q3:为什么serverSocket.accept返回的是Socket?, Socket就像耳麦(耳机加麦克风,耳机是InputStream,麦克风是OutputStream)一样,直接可以跟对方相互说话,通过Socket对象和对方进行网络通信,实现由内核实现,我们可以这样理解:accept就是说明我要指明某个客户端和这个服务器通信,而Socket就是让这两个对象可以相互说话

  • Q4:客户端服务器通信有几个流对象?, 准确来说是两个Socket对象,服务器一个客户端一个,每个Socket对象(文件)对应两个流对象,有一个流入,一个流出

  • Q4;String next读取的问题? next就是一直读数据,直到读到空白符结束(空白符是一类字符,包括不限于: 换行’\n’,回车’\r’,空格,制表符,换页符,垂直制表符)

  • **Q5: 换行和回车的区别? ** 换行(‘\n’)是让光标到下一行,回车(‘\r’)是让光标回到行首,不会另起一行

  • Q6:flush的意义是什么? IO操作是比较有开销的相比于访问内存,进行IO次数越多,程序越慢,为了提高效率,减少IO次数,使用一块内存充当缓冲区,写数据先写到缓冲区里,攒一波再进行IO,flush就可以刷新缓冲区,确保数据真的通过网卡发了出去,而不是残留在内存缓冲区,上述说的是全缓冲,而换行(‘\n’)刷新是行缓冲,行缓冲通常用于标准输入输出(控制台上打印和输入我们通常输入enter linux本质就是’\n’,windows 是 ‘\r\n’),//但我们现在是往网卡/网络文件中写(全缓冲,不会受到’\n’影响)

  • Q7: 两个TCP类的关闭与否 ? ServerSocket(只有一个,生命周期跟随程序,不关闭也没事),Socket每次来一个连接,就会有一个客户端和服务器建立连接,所以要用完就关闭

  • Q8:如何解决TPC只支持单播特性 用多线程解决此问题,在主线程建立连接,在主线程建立连接完后,创建新线程专门用于处理客户端请求,可以使用Thread创建(只要服务器系统资源足够,就几个客户端连接都可以)

  • Q9: 多个服务器重复连接再断开导致服务器频繁创建释放线程, 用线程池解决

  • Q10: 啥是C10M

    1. 10k 同一时刻,有10k个客户端(1w个客户端) , 硬件好或者线程池就能解决, C10M 就是同一时刻有1kw的客户端并发请求,这时候以达到高并发了
    2. (其实总的来说解决高并发的方法就是开源节流,开源:引入更多的硬件资源,节流:提高单位硬件资源能够处理的请求数,本质上是减少线程数量,减少资源使用)
    3. 其中的一个重要解决高并发的方法 就是IO多路复用/IO多路转接,IO多路复用就是一个线程完成多组IO操作(比如对很多小吃摊位付钱,等待他们谁先好我就直接拿摊位的食物),多路复用是OS提供的另一组网络编程的API,这组API可以配合tcp udp 的api配合使用(去搜索 java NIO 写的服务器和当前tcp的差别挺大的)

代码示例


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;
    // 此处不应该创建固定线程数目的线程池.
    private ExecutorService service = Executors.newCachedThreadPool();//线程池

    //构造方法
    // 这个操作就会绑定端口号
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }


    // 启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动!");//打印日志


        //服务器启动后在while循环时,第一步不是接收客户端请求,而是与客户端连接
        while (true) {

            //把内核中已连接的对象获取到应用程序中了,这个过程类似于生产者消费者模型
            //返回值是一个Socket
            Socket clientSocket = serverSocket.accept();


//***************************************************************************************************************
            //问题2:
            // 单个线程,不能处理多个服务器请求,只能一个结束才能处理下一个
            // 多线程解决此问题,在主线程建立连接,在主线程建立连接完后,创建新线程专门用于处理客户端请求
//            Thread t = new Thread(() -> {
//                processConnection(clientSocket);
//            });
//            t.start();


            //问题1:
            // 这个写法,是能自动关闭close,只要实现 Closeable 接口就可以这么写.
            //这么写会有其他问题.processConnection和主线程是不同线程,执行processConnection的过程中,主线程的try就执行完毕了,这就导致clientSocket没用完就被关闭了
//            try(Socket clientSocket = serverSocket.accept();){
//                Thread t = new Thread(() -> {
//                    processConnection(clientSocket);
//                });
//                t.start();
//
//            };



            // 使用线程池, 来解决上述问题
            service.submit(new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });
//***************************************************************************************************************


        }
    }

    // 通过这个方法来处理一个连接的逻辑.
    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());


        // 接下来就可以1.读取请求, 2.根据请求计算响应, 3.返回响应三步走了.
        // Socket 对象内部包含了两个字节流对象, 可以把这俩字节流对象获取到, 完成后续的读写工作
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {

            // 一次连接中, 可能会涉及到多次请求/响应
            while (true) {

                // 1. 读取请求并解析. 为了读取方便, 直接使用 Scanner.(把字节流在内地转化为字符流)
                Scanner scanner = new Scanner(inputStream);
                //当没读到客户端数据的时候hasNext会一直阻塞,或者客户端退出,hashNext感知到客户端退出就break了
                if (!scanner.hasNext()) { //如果读到eof,就读完了,就退出循环
                    // 读取完毕, 客户端下线.
                    System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }

                // 这个代码暗含一个约定, 客户端发过来的请求得是文本数据, 同时还得带有空白符作为分割. (比如换行这种)
                //next就是一直读数据,直到读到空白符结束(空白符是一类字符,包括不限于: 换行'\n',回车'\r',空格,制表符,换页符,垂直制表符)
                //换行('\n')是让光标到下一行,回车('\r')是让光标回到行首,不会另起一行
                String request = scanner.next();



                // 2. 根据请求计算响应
                String response = process(request);



                // 3. 把响应写回给客户端. 把 OutputStream 使用 PrinterWriter 包裹一下, 方便进行发数据.
                PrintWriter writer = new PrintWriter(outputStream);
                //使用 PrintWriter 的 println 方法, 把响应返回给客户端.
                //此处用 println, 而不是 print 就是为了在结尾加上 \n . 方便客户端读取响应, 使用 scanner.next 读取.
                writer.println(response);
                //这里还需要加一个 "刷新缓冲区" 操作.
                //IO操作是比较有开销的相比于访问内存,进行IO次数越多,程序越慢
                //为了提高效率,减少IO次数,使用一块内存充当缓冲区,写数据先写到缓冲区里,攒一波再进行IO,flush就可以刷新缓冲区,确保数据真的通过网卡发了出去,而不是残留在内存缓冲区
                //上述说的是全缓冲,而换行('\n')刷新是行缓冲,行缓冲通常用于标准输入输出(控制台上打印和输入我们通常输入enter linux本质就是'\n',windows 是 '\r\n'),//但我们现在是往网卡/网络文件中写(全缓冲,不会受到'\n'影响)
                writer.flush();


                // 日志, 打印当前的请求详情.
                System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(),
                        request, response);
            }
        } catch (IOException e) {
            e.printStackTrace();//捕获try的IO异常
        } finally {
            // 在 finally 中加上 close 操作, 确保当前 socket 被及时关闭!!
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    public String process(String request) {
        return request;
    }

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

5.0 TCP 客户端

问题与解决

  • Q1: 代码中Scanner 和 PintWriter 为什么可以不关闭, 流对象中持有的资源,为两个部分 1.内存(对象销毁,内存也就回收了,while循环一圈,当break自然销毁了), 2 scanner 和 printwriter 没有持有文件描述符 他们持有的是inputstram和outputstream的引用,也就是持有其他流对象的引用,而inputstram和outputstream 写在try括号内,会自动关闭的,更准确来说是socket对象持有inputstram和outputstream 只要关闭socket就可以了

代码示例

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;


    // 要和服务器通信, 就需要先知道, 服务器所在的位置.,这里的ip和端口号都是服务器的
    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        // 这个 new 操作完成之后, 就完成了 tcp 连接的建立.
        //这一段代码本质做了相当多事,在网络原理中会讲述到
        socket = new Socket(serverIp, serverPort);
    }


    public void start() {
        System.out.println("客户端启动");

        Scanner scannerConsole = new Scanner(System.in);

        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            while (true) {
                // 1. 从控制台输入字符串.
                System.out.print("-> ");
                String request = scannerConsole.next();


                // 2. 把请求发送给服务器
                PrintWriter printWriter = new PrintWriter(outputStream);
                //    使用 println 带上换行. 后续服务器读取请求, 就可以使用 scanner.next 来获取了
                printWriter.println(request);
                //    不要忘记 flush, 确保数据是真的发送出去了!!
                printWriter.flush();


                // 3. 从服务器读取响应.
                Scanner scannerNetwork = new Scanner(inputStream);
                String response = scannerNetwork.next();


                // 4. 把响应打印出来
                System.out.println(response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

_Ap0stoL

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

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

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

打赏作者

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

抵扣说明:

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

余额充值