IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> Java Socket通信之TCP协议 -> 正文阅读

[网络协议]Java Socket通信之TCP协议


紧接着 Java Socket通信之UDP协议 再来!

一、 Java流套接字通信模型

1.TCP模型

TCP的整个通信流程如下如所示:
服务器的任务:
①创建ServerSocket对象并绑定一个端口,成为listenSocket(类似于大街拉拢客人的中介)。
②listenSocket调用ServerSocket的accept()方法,把内核建立好的连接拿到代码中进行处理。如果接收到来自客户端的请求时,accept()会返回一个Socket实例,称为clientSocket(类似于中介把客人介绍给前台小姐姐,由她负责之后的业务流程,中介继续去拉拢客人),如果没有接收到请求,则一直处于阻塞等待状态。
③使用clientSocket的getInputStream和OutputStream得到字节流对象,可进行字节的读取和写入。
④当客户端断开连接后,服务器要及时关闭clientSocket,否则会出现文件资源泄露的情况。
客户端的任务:
①创建一个Socket对象,同时指定服务器的IP和Port (建立TCP连接三次握手,由内核完成,用户感知不到)。
②客户端通过Socket对象getInputStream和getOutputStream和服务器进行通信。
在这里插入图片描述
关于端口被占用问题:
通常情况下,两个进程无法绑定到同一个端口号!(除特殊情况:eg. Linux中 fork系统调用)
解决办法:如果占用端口的进程A不需要运行,就可以关闭A后,再启动需要绑定该端口的进程B;如果需要运行A进程,则可以修改进程B的绑定端口,换为其他没有使用的端口。

2.TCP Socket常见API

ServerSocket API

ServerSocket 是创建TCP服务端Socket的API;
ServerSocket构造方法:

方法说明
ServerSocket(int port)创建一个服务端流套接字socket,并绑定到响应端口上

ServerSocket方法:

方法说明
Socket accept()开始监听指定端口(创建时绑定的端口),如果有客户端连接,返回一个socket对象,如果没有,一直处于阻塞等待状态。(用户代码调用accept,才是真的把连接拿到用户代码中)
void close()关闭此套接字(否则随着连接的增多,socket文件会出现文件资源泄露的情况)

Socket API

Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端socket。不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对方端信息,即用来与对方收发数据的。
注:Socket是服务器和客户端都需要使用;ServerSocket是只在服务器中使用的
Socket 构造方法:

方法说明
Socket(String host, int post)创建一个客户端流套接字socket,并与对应IP和port的进程建立连接

Socket 方法:TCP Socket 是基于字节流的,进行具体读写时和文件类似。

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

二、TCP流套接字编程

1.回显服务器

服务器代码:

/**
 * 代码有几个值得注意的点:
 * ① 将构造好的响应写会给客户端时,要用println
 * ② 返回响应时要用flush()将缓冲区中的数据冲刷到内存中
 * ③ 当前简易的程序是一个服务器只能同时为一个客户端收发数据,如果再有一个客户端要通讯,需等待上一个客户端释放完成之后再通讯
 * 关键问题在于:如果第一个客户端没有退出,此时服务器的逻辑一直在processConnection内部打转,没有机会再次调用到accept,
 *             也没有办法处理第二个连接~
 * 解决办法:使用多线程!
 * 主线程里面循环调用accept()
 */
package TCPEcho;

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;

public class TcpEchoServer {
    private ServerSocket listenSocket = null;

    //构造函数--端口绑定
    public TcpEchoServer(int port) throws IOException {
        listenSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        //accept()相当于一个监听程序,TCP是一个有连接的操作,首先要建立连接
        //如果客户端没有发来请求,accept()阻塞等待
        //如果服务器接收到客户端发来的请求,accept()会返回一个Socket对象
        //客户端和服务器的进一步交互就交给clientSocket来完成了
        while (true) {
            Socket clientSocket = listenSocket.accept();
            processConnection(clientSocket);
        }
    }

    //处理一个请求,这个请求中可能涉及客户端和服务器的多次交互
    private 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.接收请求并解析
                //①可以使用inputStream的read()方法读取数据到byte[]中,然后在转成String
                //②第一种方法比较麻烦,还可以借助Scanner来完成这个工作
                Scanner scanner = new Scanner(inputStream);

                //如果连接关闭,才会触发到这个情况!(读取到EOF)
                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],request: %s, response: %s",
                        clientSocket.getInetAddress().toString(), clientSocket.getPort(),
                        request, response);
                System.out.println(log);
            }
        }catch(IOException e){
            e.printStackTrace();
        }finally{
            //当前的clientSocket声明周期不是跟随整个程序,而是和连接有关
            //因此需要每个服务器连接结束后进行close()操作,否则随着连接的增多,socket文件就可能出现资源泄露的情况
            clientSocket.close();
        }
    }
    //实现回显服务器(客户端发啥,服务器返回啥)
    private String process(String request) {
        return request;
    }

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

客户端代码:

package TCPEcho;

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;
    private String serverIP;
    private int serverPort;

    public TcpEchoClient(String serverIP, int serverPort) throws IOException {
        this.serverIP = serverIP;
        this.serverPort = serverPort;
        this.socket = new Socket(serverIP,serverPort); //socket创建的同时,就和服务器尝试建立连接
    }

    public void start() throws IOException {
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){
            while(true){
                //1.从键盘上读取用户输入内容
                Scanner scanner = new Scanner(System.in);
                System.out.println("->");
                String request = scanner.next();
                if(request.equals("exit")){
                    System.out.println("程序退出!");
                    break;
                }
                //2.将读取的内容构造成请求发送给服务器
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);
                printWriter.flush();
                //3.从服务器读取响应并解析
                Scanner responseScanner = new Scanner(inputStream);
                String response = responseScanner.next();
                //4.把结果显示到界面
                String log = String.format("request:%s, response:%s",request,response);
                System.out.println(log);
            }
        }catch(IOException e){
            e.printStackTrace();
        }finally{
            socket.close();
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 9090);
        tcpEchoClient.start();
    }
}

2.多线程服务器

上面的例子,我们只能实现一个客户端一个服务器这样的通信方式,如果多个客户端共同发来请求,就要用到多线程;更进一步,线程的创建和销毁都需要消耗资源,为了避免频繁创建销毁线程,我们也可以使用线程池进行代码的改进。
服务器代码:

//使用线程:
package TCPThreadEcho;

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;

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();
        }
    }


    private 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){
                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],request:%s, response:%s",
                        clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);
                System.out.println(log);
            }

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

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

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

改进:

//使用线程池:
package TCPThreadPoolEcho;

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) {
            //在这个代码中,通过创建线程,就能保证在调用完accept()之后就能立刻再次返回调用accept().
            Socket clientSocket = listenSocket.accept();
            //创建一个线程池来给客户端提供服务
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    private 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){
                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],request:%s, response:%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 {
        TcpThreadPoolEchoServer tcpThreadPoolEchoServer = new TcpThreadPoolEchoServer(9090);
        tcpThreadPoolEchoServer.start();
    }

}

客户端代码:

package TCPThreadEcho;

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 TcpThreadEchoClient {
    public Socket socket = null;
    public TcpThreadEchoClient(String serverIp, int serverPort) throws IOException {
        this.socket = new Socket(serverIp, serverPort);
    }

    public void start(){
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()){
            while (true){
                //从键盘接收请求内容
                Scanner scanner = new Scanner(System.in);
                String request = scanner.next();
                if(request.equals("exit")){
                    System.out.println("程序退出");
                    break;
                }

                //构造请求并发送
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);
                printWriter.flush();

                //接收响应并解析
                Scanner responseScanner = new Scanner(inputStream);
                String response = responseScanner.next();
                String log = String.format("[%s:%d],request:%s,response:%s",
                        socket.getInetAddress().toString(),socket.getPort(),request,response);
                System.out.println(log);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        TcpThreadEchoClient tcpThreadEchoClient = new TcpThreadEchoClient("127.0.0.1", 9090);
        tcpThreadEchoClient.start();
    }
}

针对线程特别多的情况如何处理?(高并发解决!)
①协程代替线程,完成并发
②IO多路复用机制,完成并发 IO多路复用机制
③使用分布式,提供更多的硬件资源
TCP协议的抓包工具:tcpdump(Linux)、wireShark跨平台、图形化工具。

三、TCP中的长短连接

TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据

两者对比如下:
①建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
②主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
③两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
基于BIO同步阻塞IO)的长连接会一直占用系统资源。对于并发要求很高的服务端系统来说,这样的消耗是不能承受的。实际应用时,服务端一般是基于NIO即同步非阻塞IO)来实现长连接,性能可以极大的提升。

四、协议

1. 为什么需要协议?

对于客户端及服务端应用程序来说,请求和响应,需要约定一致的数据格式:
①客户端发送请求和服务端解析请求要使用相同的数据格式。
②服务端返回响应和客户端解析响应也要使用相同的数据格式。
③请求格式和响应格式可以相同,也可以不同。
④约定相同的数据格式,主要目的是为了让接收端在解析的时候明确如何解析数据中的各个字段。
⑤可以使用知名协议(广泛使用的协议格式),如果想自己约定数据格式,就属于自定义协议。

2. 封装/分用 VS 序列化/反序列化

一般来说,在网络数据传输中,发送端应用程序,发送数据时的数据转换格式,对发送数据时的数据包装动作来说:
如果是使用知名协议,这个动作也称为封装
如果是使用小众协议(包括自定义协议),这个动作也称为序列化,一般是将程序中的对象转换为特定的数据格式。
接收端应用程序,接收数据时的数据转换格式,即对接收数据时的数据解析动作来说:
如果是使用知名协议,这个动作也称为分用
如果是使用小众协议(包括自定义协议),这个动作也称为反序列化,一般是基于接收数据特定的格式,转换为程序中的对象。

3. 自定义协议

在数据传输的过程中,除了UDP和TCP协议外,程序还存在应用层自定义协议;对于协议来说,重点需要约定好如何解析,一般是根据字段的特点来设计协议:
对于定长的字段:可以基于长度约定,如int字段,约定好4个字节即可
对于不定长的字段
①可以约定字段之间的间隔符,或最后一个字段的结束符,如换行符间隔,\n符号结束等
②除了该字段“数据”本身,再加一个长度字段,用来标识该“数据”长度;即总共使用两个字段:
“数据”字段本身,不定长,需要通过“长度”字段来解析;
“长度”字段,标识该“数据”的长度,即用于辅助解析“数据”字段。

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-03-21 21:30:49  更:2022-03-21 21:32:02 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 5:58:11-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码