Socket - TCP快速入门

Socket - TCP 快速入门

TCP 是什么

  • 英语:Transmission Control Protocol,缩写为 TCP。
  • TCP 是传输控制协议;是一种面向链接的、可靠的基于字节流传输层通讯协议,由 IETF 的 RFC 793 定义。
  • 与 UDP 同样,完成第四层传输层所指定的功能与职责。
  • 和 UDP 最大的区别是须要链接的,三次握手四次挥手,校验机制保证了数据传输的稳定性和可靠性。

TCP 的机制

  • 三次握手、四次挥手。
  • 具备校验机制、可靠、稳定的数据传输。

TCP链接、传输过程

TCP 能作什么

  1. 聊天消息传输、推送。
  2. 单人语音视频聊天。
  3. 几乎 UDP 能作的都能作,但要考虑复杂性、性能问题。
  4. TCP 没法进行广播多播的操做。
  5. 没法搜索,搜索只能 UDP 来作。

TCP 核心 API 讲解

  • socket() :建立一个客户端 Socket。
  • bind() : 绑定一个 Socket 到一个本地地址和端口上。
  • accept():服务器端接受一个新的链接。
  • write() :把数据写入到 Socket 输出流。
  • read():从 Socket 输入流中读取数据。

客户端建立流程

客户端建立流程

服务端建立流程

服务端建立流程

三次握手、四次挥手

三次握手java

  • 第一次握手:客户端发送带有 SYN 标志的链接请求报文段,而后进入 SYN_SEND 状态,等待服务端确认。
  • 第二次握手:服务端接受到客户端的 SYN 报文段后,须要发送 ACK 信息对这个 SYN 报文段进行确认。同时,还要发送本身的 SYN 请求信息。服务端会将上述信息放到一个报文段(SYN+ACK 报文段)中,一并发送给客户端,此时服务端进入 SYN_RECV 状态。
  • 第三次握手:客户端接收到服务端的 SYN+ACK 报文段后,会向服务端发送 ACK 确认报文段,这个报文段发送完毕后,客户端和服务端都进入 ESTABLEISHED 状态,完成 TCP 三次握手。

TCP 三次握手

四次挥手算法

当被动方收到主动方的 FIN 报文通知时,它仅仅表示主动方没有数据再发送给被动方了。但未必被动方全部的数据都完整的发送给了主动方,因此被动方不会立刻关闭 SOCKET,它可能还须要发送一些数据给主动方后,再发送 FIN 报文给主动方,告诉主动方赞成关闭链接,因此这里的 ACK 报文和 FIN 报文多数状况下都是分开发送的。服务器

原理:网络

  1. 第一次挥手:Client 发送一个 FIN,用来关闭 Client 到 Server 的数据传送,Client 进入 FIN_WAIT_1 状态。
  2. 第二次挥手:Server 收到 FIN 后,发送一个 ACK 给 Client,确认序号为收到序号+1(与 SYN 相同,一个 FIN 占用一个序号),Server 进入 CLOSE_WAIT 状态。
  3. 第三次挥手:Server 发送一个 FIN,用来关闭 Server 到 Client 的数据传送,Server 进入 LAST_ACK 状态。
  4. 第四次挥手:Client 收到 FIN 后,Client 进入 TIME_WAIT 状态,接着发送一个 ACK 给 Server,确认序号为收到序号+1,Server 进入 CLOSED 状态,完成四次挥手。

TCP 传输可靠性

  • 校验和

    发送的数据包的二进制相加而后取反,目的是检测数据在传输过程当中的任何变化。若是收到报文段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。并发

  • 确认应答与序列号

    TCP 给发送的每个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。异步

  • 超时重传

    TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。若是不能及时收到一个确认,将重发这个报文段。socket

  • 流量控制

    TCP 链接的每一方都有固定大小的缓冲空间,TCP 的接收端只容许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方下降发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。ide

  • 拥塞控制

    当网络拥塞时,减小数据的发送。函数

应用数据被分割成 TCP 认为最适合发送的数据块。性能

TCP 的接收端会丢弃重复的数据

TCP 数据发送流程

TCP数据发送流程

排序、组装流程

因为数据传输的顺序多是不必定的,因此在此中间会进行排序。

排序、组装流程

数据包丢失,进行重传

数据包丢失,进行重传

链接中断

当链接中断后,须要进行重连、而后从新进行重传。

链接中断

一对多传输流程

一对多传输流程

TCP 基础数据传输案例代码

public class Server {
    private static final int PORT = 20000;

    public static void main(String[] args) throws IOException {
        ServerSocket server = createServerSocket();
        initServerSocket(server);
        // 绑定到本地端口上
        server.bind(new InetSocketAddress(Inet4Address.getLocalHost(), PORT), 50);
        System.out.println("服务器准备就绪~");
        System.out.println("服务器信息:" + server.getInetAddress() + " P:" + server.getLocalPort());
        // 等待客户端链接
        for (; ; ) {
            // 获得客户端
            Socket client = server.accept();
            // 客户端构建异步线程
            ClientHandler clientHandler = new ClientHandler(client);
            // 启动线程
            clientHandler.start();
        }

    }

    private static ServerSocket createServerSocket() throws IOException {
        // 建立基础的ServerSocket
        ServerSocket serverSocket = new ServerSocket();
        // 绑定到本地端口20000上,而且设置当前可容许等待连接的队列为50个
        //serverSocket = new ServerSocket(PORT);
        // 等效于上面的方案,队列设置为50个
        //serverSocket = new ServerSocket(PORT, 50);
        // 与上面等同
        // serverSocket = new ServerSocket(PORT, 50, Inet4Address.getLocalHost());
        return serverSocket;
    }

    private static void initServerSocket(ServerSocket serverSocket) throws IOException {
        // 是否复用未彻底关闭的地址端口
        serverSocket.setReuseAddress(true);
        // 等效Socket#setReceiveBufferSize
        serverSocket.setReceiveBufferSize(64 * 1024 * 1024);
        // 设置serverSocket#accept超时时间
        // serverSocket.setSoTimeout(2000);
        // 设置性能参数:短连接,延迟,带宽的相对重要性
        serverSocket.setPerformancePreferences(1, 1, 1);
    }

    /**
     * 客户端消息处理
     */
    private static class ClientHandler extends Thread {
        private Socket socket;

        ClientHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            super.run();
            System.out.println("新客户端链接:" + socket.getInetAddress() + " P:" + socket.getPort());
            try {
                // 获得套接字流
                OutputStream outputStream = socket.getOutputStream();
                InputStream inputStream = socket.getInputStream();
                byte[] buffer = new byte[256];
                int readCount = inputStream.read(buffer);
                ByteBuffer byteBuffer = ByteBuffer.wrap(buffer, 0, readCount);
                // byte
                byte be = byteBuffer.get();
                // char
                char c = byteBuffer.getChar();
                // int
                int i = byteBuffer.getInt();
                // bool
                boolean b = byteBuffer.get() == 1;
                // Long
                long l = byteBuffer.getLong();
                // float
                float f = byteBuffer.getFloat();
                // double
                double d = byteBuffer.getDouble();
                // String
                int pos = byteBuffer.position();
                String str = new String(buffer, pos, readCount - pos - 1);
                System.out.println("收到数量:" + readCount + " 数据:" + be + "\n" + c + "\n" + i + "\n" + b + "\n" + l + "\n" + f + "\n" + d + "\n" + str + "\n");
                outputStream.write(buffer, 0, readCount);
                outputStream.close();
                inputStream.close();

            } catch (Exception e) {
                System.out.println("链接异常断开");
            } finally {
                // 链接关闭
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("客户端已退出:" + socket.getInetAddress() + " P:" + socket.getPort());
        }
    }
}
public class Client {
    private static final int PORT = 20000;
    private static final int LOCAL_PORT = 20001;

    public static void main(String[] args) throws IOException {
        Socket socket = createSocket();
        initSocket(socket);
        // 连接到本地20000端口,超时时间3秒,超过则抛出超时异常
        socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(), PORT), 3000);
        System.out.println("已发起服务器链接,并进入后续流程~");
        System.out.println("客户端信息:" + socket.getLocalAddress() + " P:" + socket.getLocalPort());
        System.out.println("服务器信息:" + socket.getInetAddress() + " P:" + socket.getPort());
        try {
            // 发送接收数据
            todo(socket);
        } catch (Exception e) {
            System.out.println("异常关闭");
        }
        // 释放资源
        socket.close();
        System.out.println("客户端已退出~");

    }

    private static Socket createSocket() throws IOException {
        /*
        // 无代理模式,等效于空构造函数
        Socket socket = new Socket(Proxy.NO_PROXY);
        // 新建一份具备HTTP代理的套接字,传输数据将经过www.baidu.com:8080端口转发
        Proxy proxy = new Proxy(Proxy.Type.HTTP,
                new InetSocketAddress(Inet4Address.getByName("www.baidu.com"), 8800));
        socket = new Socket(proxy);
        // 新建一个套接字,而且直接连接到本地20000的服务器上
        socket = new Socket("localhost", PORT);
        // 新建一个套接字,而且直接连接到本地20000的服务器上
        socket = new Socket(Inet4Address.getLocalHost(), PORT);
        // 新建一个套接字,而且直接连接到本地20000的服务器上,而且绑定到本地20001端口上
        socket = new Socket("localhost", PORT, Inet4Address.getLocalHost(), LOCAL_PORT);
        socket = new Socket(Inet4Address.getLocalHost(), PORT, Inet4Address.getLocalHost(), LOCAL_PORT);
        */
        Socket socket = new Socket();
        // 绑定到本地20001端口
        socket.bind(new InetSocketAddress(Inet4Address.getLocalHost(), LOCAL_PORT));

        return socket;
    }

    private static void initSocket(Socket socket) throws SocketException {
        // 设置读取超时时间为2秒
        socket.setSoTimeout(2000);
        // 是否复用未彻底关闭的Socket地址,对于指定bind操做后的套接字有效
        socket.setReuseAddress(true);
        // 是否开启Nagle算法
        socket.setTcpNoDelay(true);
        // 是否须要在长时无数据响应时发送确认数据(相似心跳包),时间大约为2小时
        socket.setKeepAlive(true);
        // 对于close关闭操做行为进行怎样的处理;默认为false,0
        // false、0:默认状况,关闭时当即返回,底层系统接管输出流,将缓冲区内的数据发送完成
        // true、0:关闭时当即返回,缓冲区数据抛弃,直接发送RST结束命令到对方,并没有需通过2MSL等待
        // true、200:关闭时最长阻塞200毫秒,随后按第二状况处理
        socket.setSoLinger(true, 20);
        // 是否让紧急数据内敛,默认false;紧急数据经过 socket.sendUrgentData(1);发送
        socket.setOOBInline(true);
        // 设置接收发送缓冲器大小
        socket.setReceiveBufferSize(64 * 1024 * 1024);
        socket.setSendBufferSize(64 * 1024 * 1024);
        // 设置性能参数:短连接,延迟,带宽的相对重要性
        socket.setPerformancePreferences(1, 1, 0);
    }

    private static void todo(Socket client) throws IOException {
        // 获得Socket输出流
        OutputStream outputStream = client.getOutputStream();
        // 获得Socket输入流
        InputStream inputStream = client.getInputStream();
        byte[] buffer = new byte[256];
        ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
        // byte
        byteBuffer.put((byte) 126);
        // char
        char c = 'a';
        byteBuffer.putChar(c);
        // int
        int i = 2323123;
        byteBuffer.putInt(i);
        // bool
        boolean b = true;
        byteBuffer.put(b ? (byte) 1 : (byte) 0);
        // Long
        long l = 298789739;
        byteBuffer.putLong(l);
        // float
        float f = 12.345f;
        byteBuffer.putFloat(f);
        // double
        double d = 13.31241248782973;
        byteBuffer.putDouble(d);
        // String
        String str = "Hello你好!";
        byteBuffer.put(str.getBytes());
        // 发送到服务器
        outputStream.write(buffer, 0, byteBuffer.position() + 1);
        // 接收服务器返回
        int read = inputStream.read(buffer);
        System.out.println("收到数量:" + read);
        // 资源释放
        outputStream.close();
        inputStream.close();
    }
}
相关文章
相关标签/搜索