TCP 理论详解

TCP 简 介

  • TCP(Transmission Control Protocol) 是 socket 上的一种提供可靠的数据传输的通信协议——传输控制协议
  • TCP 只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。就像操作系统会提供标准的编程接口,比如 Win32 编程接口一样,TCP 也必须对外提供编程接口,这就是 Socket 编程接口 

  • TCP/IP 协议是一个协议簇,包括应用层,传输层,网络层,网络访问层,之所以命名为TCP/IP协议,因为TCP、IP协议是两个很重要的协议。TCP 协议只是 TCP/IP 协议簇下的其中一个。

  • 通过 IP 协议并不能清楚地了解到数据包是否顺利地发送给目标计算机,而使用 TCP 协议,它将数据包成功发送给目标计算机后,会要求发送一个确认,如果在某个时间内没有收到确认,TCP将重新发送数据包。
  • Socket(套接字) 实际上是对 TCP 协议的封装,Socket 本身并不是协议,而是一个调用接口(API),通过 Socket 才能使用 TCP协议
  • Socket编程基本就是listen,accept以及send,write等几个基本的操作 ,Java JDK 中 Socket 编程的 API 都在 java.net 包中

TCP VS UDP

  • TCP(Transmission Control Protocol) 可以保证数据的正确性和可靠性,UDP 则允许数据丢失
  • TCP 和 UDP(User Datagram Protocol   用户数据报协议)同属于传输层,共同架设在IP层(网络层)之上
  • IP 层主要负责节点之间(End to End)的数据包传送,这里的节点是一台网络设备,比如计算机,只负责把数据送到节点,而不能区分上面的不同应用,所以 TCP 和 UDP 协议在其基础上加入了端口信息,端口于是标识的是一个节点上的一个应用
  • 除了增加端口信息,UPD 协议基本就没有对 IP 层的数据进行任何的处理了,而TCP协议还加入了更加复杂的传输控制,比如滑动的数据发送窗口(Slice Window),以及接收确认和重发机制以达到数据的可靠传送
  • 采用 UDP 的 QQ 比采用 TCP 传输协议的 MSN 传输文件要快,但并不能说 QQ 的通信是不安全的,因为程序员可以把确认、验证的工作交给应用程序来做
  • TCP 协议提供了可靠的数据传输,但是其拥塞控制、数据校验、重传机制的网络开销很大,不适合实时通信,所以选择开销很小的 UDP 协议来传输数据

对比项

TCP

UDP

说明

对系统要求

较多

 

程序结构

较复杂

简单

UDP是流模式、数据报模式

数据准确性

UDP可能丢包

数据顺序

保证顺序

不保证顺序

 

数据大小

较大

短消息

 

客户端数目

较少

大量

 

响应速度

较慢

 

网络负担

较大

 

  • UDP 就像发短信,只管发出去,至于对方是不是空号(网络不可到达)能不能收到(丢包)等并不关心;TCP 就像打电话,双方要通话,首先,要确定对方是不是开机(网络可以到达),然后要确定是不是没有信号(网络可以到达),然后还需要对方接听(通信链接)。

TCP VS HTTP

  • TPC 协议是传输层协议,主要解决数据如何在网络中传输而 HTTP 是应用层协议,主要解决如何包装数据
  • 传输数据时,可以只使用(传输层)TCP 协议,但是这样就没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有很多,比如 HTTP、FTP、TELNET 等,也可以自己定义应用层协议。WEB 使用 HTTP 协议作应用层协议,以封装 HTTP 文本信息,然后使用 TCP 做传输层协议将它发到网络上。
  • 套接字(Socket)是通信的基石,是支持 TCP/IP 协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息

1)连接使用的协议

2)本地主机的IP地址

3)本地进程的协议端口

4)远地主机的IP地址

5)远地进程的协议端口

  •  应用层通过传输层进行数据通信时,TCP 会遇到同时为多个应用程序进程提供并发服务的问题,为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与 TCP/IP 协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket 接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。

TCP 三次握手

  • TCP 是面向连接的,虽然说网络的不安全不稳定特性决定了多少次握手都不能保证连接的可靠性,但 TCP 的三次握手在很大程度上保证了连接的可靠性。
  • 建立起一个 TCP 连接需要经过“三次握手”

第一次握手:客户端发送 syn (Synchronization:同步) 包 (syn=j) 到服务器,并进入SYN_SEND 状态,等待服务器确认

第二次握手:服务器收到 syn 包,必须确认客户的 SYN (ack=j+1),同时自己也发送一个 SYN包(syn=k),即SYN+ACK (Acknowledge:承认)包,此时服务器进入SYN_RECV(Receive:接收)状态;

第三次握手:客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(Established:建立、确立) 状态,完成三次握手。

  • 握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP 连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去
  • 服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次握手” 。

TCP 与 Java

  • 协议相当于相互通信的程序间达成的一种约定,它规定了分组报文的结构、交换方式、包含的意义以及怎样对报文所包含的信息进行解析
  • TCP/IP 协议族有 IP 协议、TCP 协议和 UDP 协议
  • 现在 TCP/IP 协议族中的主要 socket 类型为流套接字(使用TCP协议)和数据报套接字(使用UDP协议)
  • TCP 协议提供面向连接的服务,通过它建立的是可靠地连接Java   为 TCP 协议提供了两个类:Socket 类和 ServerSocket
  • 一个 Socket 实例代表了 TCP 连接的一个客户端,而一个 ServerSocket 实例代表了 TCP 连接的一个服务器端
  • 一般在 TCP Socket 编程中,客户端有多个,而服务器端只有一个,客户端 TCP 向服务器端 TCP 发送连接请求,服务器端的 ServerSocket 实例则监听来自客户端的 TCP 连接请求,并为每个请求创建新的 Socket 实例,由于服务端在调用 accept() 等待客户端的连接请求时会阻塞,直到收到客户端发送的连接请求才会继续往下执行代码,因此要为每个Socket连接开启一个线程
  • 服务器端要同时处理 ServerSocket 实例和 Socket 实例,而客户端只需要使用 Socket 实例。另外每个 Socket 实例会关联一个 InputStream 和 OutputStream 对象,通过将字节写入套接字的 OutputStream 来发送数据,并通过从 InputStream 来接收数据。
  • 套接字之间的连接过程分为三个步骤:1、服务器监听;2、客户端请求;3、连接确认,4)收发消息。

1)服务器监听

  • 服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求
    /**
     * 创建绑定到特定端口的 TCP 服务端实例
     * ServerSocket(int port):指定绑定的端口,默认的tcp队列大小为50,默认监听本地所有的ip地址(如果有多个网卡)
     * ServerSocket(int port, int backlog, InetAddress bindAddr)
     *      port)绑定的端口
     *      backlog)TCP连接队列大小
     *      bindAddr)多网卡时指定绑定哪个 IP 地址
     * 如果被绑定的端口已经被其它应用绑定,如 Mysql 的 3306,Tomcat 的 8080 等,
     * 则此时绑定会抛出异常:java.net.BindException: Address already in use:
     */
    ServerSocket serverSocket = new ServerSocket(8080);

    /**
     * 侦听 TCP 客户端的连接
     * 该方法会从全连接队列中获取一个客户端Socket请求。
     * 该方法是阻塞方法,如果当前没有请求的连接,则会一直阻塞,直到有客户端连接请求为止
     * 服务器端的 ServerSocket 实例则监听来自客户端的 TCP 连接请求,并为每个请求创建新的 Socket 实例
     */
    Socket socket = serverSocket.accept();

    System.out.println("客户端连接成功:" + socket.getRemoteSocketAddress());

2)客户端请求

  • 客户端的套接字( Socket )需要知道服务器端套接字(ServerSocket)的地址和端口号,然后向服务器端套接字提出连接请求
    /**
     * Socket(String host, int port):
     *      host)被连接的服务器 IP 地址
     *      port)被连接的服务器监听的端口
     * Socket(InetAddress address, int port)
     *      address)用于设置 ip 地址的对象
     * 此时如果 TCP 服务器未开放,或者其它原因导致连接失败,则抛出异常:
     * java.net.ConnectException: Connection refused: connect
     */
    Socket socket = new Socket("127.0.0.1", 8080);
    System.out.println("连接成功..........");

3)连接确认

  • 当服务器端套接字( ServerSocket )监听到或者说接收到客户端套接字( Socket) 的连接请求时,就响应客户端套接字的请求,并把服务器端套接字的描述返回给客户端,一旦客户端确认了此描述,双方就正式建立连接。
  • 注意,应该使用多线程,每监听到一个,就新开一个线程来处理,使服务器端套接字一直处于监听状态,继续接收其他客户端套接字的连接请求

4)收发消息

  • TCP 服务器端编码:
package com.lct.tcp;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Created by Administrator on 2018/10/14 0014.
 * TCP 服务端
 */
public class TcpServer {

    public static void main(String[] args) {
        tcpAccept();
    }

    /**
     * Tcp 服务端介绍连接
     */
    public static void tcpAccept() {
        ServerSocket serverSocket = null;
        try {
            /**
             * 创建绑定到特定端口的 TCP 服务端实例
             * ServerSocket(int port):指定绑定的端口,默认的tcp队列大小为50,默认监听本地所有的ip地址(如果有多个网卡)
             * ServerSocket(int port, int backlog, InetAddress bindAddr)
             *      port)绑定的端口
             *      backlog)TCP连接队列大小
             *      bindAddr)多网卡时指定绑定哪个 IP 地址
             * 如果被绑定的端口已经被其它应用绑定,如 Mysql 的 3306,Tomcat 的 8080 等,
             * 则此时绑定会抛出异常:java.net.BindException: Address already in use:
             */
            serverSocket = new ServerSocket(8080);

            /**
             * 侦听 TCP 客户端的连接,TCP 是典型的 BIO 模型,即同步则塞式网络编程,必须保证循环不间断的监听客户端的连接
             * 对于每一个 TCP 的客户端连接都要新开线程进行数据处理
             * accept() 方法会从全连接队列中获取一个客户端Socket请求。
             * accept() 方法是阻塞方法,如果当前没有请求的连接,则会一直阻塞,直到有客户端连接请求为止
             * 服务器端的 ServerSocket 实例则监听来自客户端的 TCP 连接请求,并为每个请求创建新的 Socket 实例
             */
            while (true) {
                System.out.println("等待客户端连接......" + Thread.currentThread().getName());
                final Socket socket = serverSocket.accept();

                /** 设置输入流读取数据的超时时间为 10 秒*/
                /*socket.setSoTimeout(10 * 1000);*/
                System.out.println("客户端连接成功......" + Thread.currentThread().getName());
                new Thread() {
                    @Override
                    public void run() {
                        try {
                            InputStream inputStream = socket.getInputStream();
                            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
                            /** 使用 BufferedReader 逐行读取更方便*/
                            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
                            StringBuffer message = new StringBuffer();
                            String readline = null;

                            /**
                             * TCP 连接成功之后,输入流 InputStream 读取数据的方法也会一直阻塞等待接收对方的数据
                             * 当使用了 Socket 的 setSoTimeout(int timeout) 方法设置超时时间后,则在指定的时间内没有
                             * 接收倒是数据时,则抛异常:java.net.SocketTimeoutException: Read timed out
                             */
                            while ((readline = bufferedReader.readLine()) != null) {
                                message.append(readline + "\n");
                            }
                            System.out.println(Thread.currentThread().getName() + " 收到消息:" + message.toString());
                            bufferedReader.close();
                            inputStreamReader.close();
                            inputStream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        } finally {
                            /**操作完毕,关闭 Socket 连接*/
                            try {
                                if (!socket.isClosed()) {
                                    socket.close();
                                }
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            /**
             * 如果抛出异常,则关闭 Tcp 服务器
             */
            if (serverSocket != null && !serverSocket.isClosed()) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • TCP 客户端端编码:
package com.lct.tcp;

import java.io.*;
import java.net.Socket;

/**
 * Created by Administrator on 2018/10/14 0014.
 * TCP 客户端
 */
public class TcpClient {
    public static void main(String[] args) {
        /**
         * 使用三个线程模拟 3 个 TCP 客户端进行连接
         * 连接成功后,给服务器发送一条消息
         */
        for (int i = 0; i < 3; i++) {
            new Thread() {
                @Override
                public void run() {
                    tcpSendMessage();
                }
            }.start();
        }
    }

    /**
     * Tcp 客户端连接服务器并发送消息
     */
    public static void tcpSendMessage() {
        Socket socket = null;
        try {
            /**
             * Socket(String host, int port):
             *      host)被连接的服务器 IP 地址
             *      port)被连接的服务器监听的端口
             * Socket(InetAddress address, int port)
             *      address)用于设置 ip 地址的对象
             * 此时如果 TCP 服务器未开放,或者其它原因导致连接失败,则抛出异常:
             * java.net.ConnectException: Connection refused: connect
             */
            socket = new Socket("127.0.0.1", 8080);
            System.out.println("连接成功.........." + Thread.currentThread().getName());

            /** 往服务端发送一条消息,指定字符编码为 UTF-8*/
            OutputStream outputStream = socket.getOutputStream();
            OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, "UTF-8");
            outputStreamWriter.write("修长城的民族!,客户端=" + Thread.currentThread().getName());
            outputStreamWriter.flush();
            outputStreamWriter.close();
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            /** 操作完毕关闭 socket*/
            if (socket != null && !socket.isClosed()) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行结果,服务端如下:

等待客户端连接......main
客户端连接成功......main
等待客户端连接......main
客户端连接成功......main
等待客户端连接......main
客户端连接成功......main
等待客户端连接......main
Thread-0 收到消息:修长城的民族!,客户端=Thread-2

Thread-1 收到消息:修长城的民族!,客户端=Thread-0

Thread-2 收到消息:修长城的民族!,客户端=Thread-1

运行结果,客户端如下:

连接成功..........Thread-1
连接成功..........Thread-2
连接成功..........Thread-0

网络编程

  • 技术日新月异,目前网络编程最为流行的当属 Netty,Java 网络编程发展历程:

JDK 1.4 以前:java.net + java.io——即平时所使用的简单的 TCP 、UDP 编程

JDK 1.4 及以后:java.nio

当下流行:JBoos 的 Netty 库、Apache 的  Mina 等

猜你喜欢

转载自blog.csdn.net/wangmx1993328/article/details/83044517