美文网首页MacOS, Java和IDEA程序员
网络协议、端口和Socket

网络协议、端口和Socket

作者: SpaceCat | 来源:发表于2019-09-27 08:57 被阅读0次

    1、网络协议分层

    网络层次可划分为五层因特网协议栈和七层因特网协议栈。

    1.1 五层因特网协议栈

    因特网协议栈共有五层:应用层、传输层、网络层、链路层和物理层。不同于OSI七层模型这也是实际使用中使用的分层方式。
    (1)应用层
    支持网络应用,应用协议仅仅是网络应用的一个组成部分,运行在不同主机上的进程则使用应用层协议进行通信。主要的协议有:http、ftp、telnet、smtp、pop3等。
    (2)传输层
    负责为信源和信宿提供应用程序进程间的数据传输服务,这一层上主要定义了两个传输协议,传输控制协议即TCP和用户数据报协议UDP。
    (3)网络层
    负责将数据报独立地从信源发送到信宿,主要解决路由选择、拥塞控制和网络互联等问题。
    (4)数据链路层
    负责将IP数据报封装成合适在物理网络上传输的帧格式并传输,或将从物理网络接收到的帧解封,取出IP数据报交给网络层。
    (5)物理层
    负责将比特流在结点间传输,即负责物理传输。该层的协议既与链路有关也与传输介质有关。

    1.2 七层因特网协议栈

    ISO提出的OSI(Open System Interconnection)模型将网络分为七层,即物理层( Physical )、数据链路层(Data Link)、网络层(Network)、传输层(Transport)、会话层(Session)、表示层(Presentation)和应用层(Application)。
    OSI模型共分七层:从上至下依次是 应用层指网络操作系统和具体的应用程序,对应WWW服务器、FTP服务器等应用软件 表示层数据语法的转换、数据的传送等 会话层 建立起两端之间的会话关系,并负责数据的传送 传输层 负责错误的检查与修复,以确保传送的质量,是TCP工作的地方。(报文) 网络层 提供了编址方案,IP协议工作的地方(数据包) 数据链路层将由物理层传来的未经处理的位数据包装成数据帧 物理层 对应网线、网卡、接口等物理设备(位)。
    (1)物理层
    物理层(Physical layer)是参考模型的最低层。该层是网络通信的数据传输介质,由连接不同结点的电缆与设备共同构成。主要功能是:利用传输介质为数据链路层提供物理连接,负责处理数据传输并监控数据出错率,以便数据流的透明传输。
    (2)数据链路层
    数据链路层(Data link layer)是参考模型的第2层。 主要功能是:在物理层提供的服务基础上,在通信的实体间建立数据链路连接,传输以“帧”为单位的数据包,并采用差错控制与流量控制方法,使有差错的物理线路变成无差错的数据链路。
    (3)网络层
    网络层(Network layer)是参考模型的第3层。主要功能是:为数据在结点之间传输创建逻辑链路,通过路由选择算法为分组通过通信子网选择最适当的路径,以及实现拥塞控制、网络互联等功能。
    (4)传输层
    传输层(Transport layer)是参考模型的第4层。主要功能是向用户提供可靠的端到端(End-to-End)服务,处理数据包错误、数据包次序,以及其他一些关键传输问题。传输层向高层屏蔽了下层数据通信的细节,因此,它是计算机通信体系结构中关键的一层。
    (5)会话层
    会话层(Session layer)是参考模型的第5层。主要功能是:负责维护两个结点之间的传输链接,以便确保点到点传输不中断,以及管理数据交换等功能。
    (6)表示层
    表示层(Presentation layer)是参考模型的第6层。主要功能是:用于处理在两个通信系统中交换信息的表示方式,主要包括数据格式变换、数据加密与解密、数据压缩与恢复等功能。
    (7)应用层
    应用层(Application layer)是参考模型的最高层。主要功能是:为应用软件提供了很多服务,例如文件服务器、数据库服务、电子邮件与其他网络软件服务。

    1.3 对应关系图

    Layer Mapping

    2、TCP/IP协议中的IP地址和端口号

    TCP header

    上图是一个TCP/IP数据包头部的结构概要。
    当互联网中的一个节点要想另一个节点发送数据的时候,它需要两个信息:

    • 数据发送目标节点的IP地址,用来标识数据要送往哪个节点。这里的IP地址信息,在上图的IPv4协议包头里面,这里没有展开。
    • 数据发送目标节点的端口号,用来标识这些发送的数据将由目标节点上的什么服务接收处理。因为目标节点上可能启着好多服务,比如http、ftp、telnet等。每个服务会监听一个专门的端口号,这样,当发送方制定了目的端口号之后,也就指定了目标节点上接收处理该数据包的服务。从上图可以看出,端口号出现在TCP协议头部分,分为目标端口号和源端口号。

    注:这里的目的端口号是8080,看起来有些奇怪,这个应该是一个代理服务器的端口也就是说,数据包先发到代理服务器的8080端口,然后再由代理服务器将数据包转发出去。

    2.0 端口号分类

    第一类公认端口(Well Known Ports):从0到1023,它们紧密绑定(binding)于一些服务。通常这些端口的通讯明确表明了某种服务的协议,必须要有Root权限才能绑定。例如:80端口实际上总是HTTP通讯。
    第二类注册端口(Registered Ports):从1024到49151。它们松散地绑定于一些服务。也就是说有许多服务绑定于这些端口,这些端口同样用于许多其它目的。例如:许多系统处理动态端口从1024左右开始。
    第三类动态和/或私有端口(Dynamic, private or ephemeral ports):从49152到65535。理论上,不应为服务分配这些端口。
    实际上,机器通常从1024起分配动态端口。但也有例外:SUN的RPC端口从32768开始。

    Well-known ports
    The port numbers in the range from 0 to 1023 (0 to 2^10 − 1) are the well-known ports or system ports. They are used by system processes that provide widely used types of network services. On Unix-like operating systems, a process must execute with superuser privileges to be able to bind a network socket to an IP address using one of the well-known ports.
    Registered ports
    The range of port numbers from 1024 to 49151 (2^10 to 2^14 + 2^15 − 1) are the registered ports. They are assigned by IANA for specific service upon application by a requesting entity. On most systems, registered ports can be used without superuser privileges.
    Dynamic, private or ephemeral ports
    The range 49152–65535 (2^15 + 2^14 to 2^16 − 1) contains dynamic or private ports that cannot be registered with IANA. This range is used for private or customized services, for temporary purposes, and for automatic allocation of ephemeral ports.
    From Wikipedia.

    2.1 目的端口号

    目的端口号用于指定数据包传到目标服务器之后,会被哪个程序接收处理。如下面两个图。
    如果发送的目的端口号是80,那么数据包会被目标服务器上的http服务器端处理程序接受处理:


    http

    如果发送的目的端口号是21,那么数据包会被目标服务器上的ftp服务器端处理程序接受处理:


    ftp

    不难理解,同样地,如果发送的目的端口号是23,那么数据包会被目标服务器上的telnet服务器端处理程序接受处理。

    2.2 源端口号

    将数据包发送到目标节点时,提供源端口号是为了让目标节点上接受处理数据包的服务器程序能够将返回数据包发送到客户端正确的会话上。
    实际上,当服务器端程序收到数据包之后,会将数据包的源端口和目的端口反转,这样就能够将数据发送到正确的客户端程序上了。


    Source Port

    As Host A receives the Internet Server's reply, the Transport layer will notice the reversed ports and recognise it as a response to the previous packet it sent (the one with the green arrow).
    The Transport and Session layers keep track of all new connections, established connections and connections that are in the process of being torn down, which explains how Host A remembers that it's expecting a reply from the Internet Server.

    这个源端口号就是从上面提到的端口分类中的第三类端口中,任意分配的一个端口号。至于这个端口选择的范围,视具体的操作系统而定,有的操作系统上还能够提供命令来修改这个范围。

    3、Java中的Socket编程

    套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。
    传输层实现端到端的通信,因此,每一个传输层连接有两个端点。那么,传输层连接的端点是什么呢?不是主机,不是主机的IP地址,不是应用进程,也不是传输层的协议端口。传输层连接的端点叫做套接字(socket)。根据RFC793的定义:端口号拼接到IP地址就构成了套接字。所谓套接字,实际上是一个通信端点,每个套接字都有一个套接字序号,包括主机的IP地址与一个16位的主机端口号,即形如(主机IP地址:端口号)。例如,如果IP地址是210.37.145.1,而端口号是23,那么得到套接字就是(210.37.145.1:23)。
    套接字可以看成是两个网络应用程序进行通信时,各自通信连接中的一个端点。通信时,其中的一个网络应用程序将要传输的一段信息写入它所在主机的Socket中,该Socket通过网络接口卡的传输介质将这段信息发送给另一台主机的Socket中,使这段信息能传送到其他程序中。因此,两个应用程序之间的数据传输要通过套接字来完成。
    在网络应用程序设计时,由于TCP/IP的核心内容被封装在操作系统中,如果应用程序要使用TCP/IP,可以通过系统提供的TCP/IP的编程接口来实现。在Windows环境下,网络应用程序编程接口称作Windows Socket。为了支持用户开发面向应用的通信程序,大部分系统都提供了一组基于TCP或者UDP的应用程序编程接口(API),该接口通常以一组函数的形式出现,也称为套接字(Socket)。

    3.1 简单例子:单次收发

    创建一个Maven模块simpletest-parent作为父模块,位于目录/Users/chengxia/Developer/Java/simpletest-parent,pom.xml配置文件如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.lfqy.socket</groupId>
        <artifactId>simpletest-parent</artifactId>
        <packaging>pom</packaging>
        <version>1.0-SNAPSHOT</version>
        <modules>
            <module>simpletest-server</module>
            <module>simpletest-client</module>
        </modules>
    
    
    </project>
    

    创建一个子模块simpletest-server作为Socket服务器端,位于/Users/chengxia/Developer/Java/simpletest-parent/simpletest-server,pom.xml配置文件如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>simpletest-parent</artifactId>
            <groupId>com.lfqy.socket</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.lfqy.socket</groupId>
        <artifactId>simpletest-server</artifactId>
    
    
    </project>
    

    创建一个子模块simpletest-client作为Socket客户端,位于/Users/chengxia/Developer/Java/simpletest-parent/simpletest-client,pom.xml配置文件如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>simpletest-parent</artifactId>
            <groupId>com.lfqy.socket</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.lfqy.socket</groupId>
        <artifactId>simpletest-client</artifactId>
    
    
    </project>
    

    整个项目的目录结构如下图:


    Project Structure

    相关的代码如下。
    com.lfqy.socket.client.ClientSocket0Test:

    package com.lfqy.socket.client;
    
    import java.io.BufferedWriter;
    import java.io.IOException;
    import java.io.OutputStreamWriter;
    import java.net.Socket;
    
    /**
     * Created by chengxia on 2019/9/23.
     */
    public class ClientSocket0Test {
        public static void main(String []args){
            try {
                Socket socket =new Socket("127.0.0.1",9999);
                BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                String str="Hello, Server! I am Client.";
                bufferedWriter.write(str);
                //刷新输入流
                bufferedWriter.flush();
                //关闭socket的输出流
                socket.shutdownOutput();
            }catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    com.lfqy.socket.server.ServerSocket0Test:

    package com.lfqy.socket.server;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    /**
     * Created by chengxia on 2019/9/23.
     */
    public class ServerSocket0Test {
        public static void main(String []args){
            try {
                // 初始化服务端socket并且绑定9999端口
                ServerSocket serverSocket  = new ServerSocket(9999);
                //等待客户端的连接
                Socket socket = serverSocket.accept();
                //获取输入流
                BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream()));
                //读取一行数据
                String str = bufferedReader.readLine();
                //输出打印
                System.out.println(str);
            }catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    在启动时,先运行com.lfqy.socket.server.ServerSocket0Test启动服务器端,运行成功之后,控制台没有任何输出,程序在等客户端连接。然后,运行com.lfqy.socket.client.ClientSocket0Test启动客户端,连接并向服务器端发送数据,运行成功之后,服务器端程序的输出如下,显示发送成功:

    Hello, Server! I am Client.
    
    Process finished with exit code 0
    

    3.2 连续收发

    基于前面的例子,在和com.lfqy.socket.client.ClientSocket0Test相同包下,创建com.lfqy.socket.client.ClientSocket1Test作为客户端,如下。

    package com.lfqy.socket.client;
    
    import java.io.*;
    import java.net.Socket;
    
    /**
     * Created by chengxia on 2019/9/23.
     */
    public class ClientSocket1Test {
        public static void main(String []args){
            try {
                Socket socket =new Socket("127.0.0.1",9999);
                //通过socket获取字符流
                BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                //通过标准输入流获取字符流
                BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(System.in,"UTF-8"));
    
                while (true){
                    String str = bufferedReader.readLine();
                    bufferedWriter.write(str);
                    bufferedWriter.write("\n");
                    bufferedWriter.flush();
                    System.out.println("Sent: " + str);
                }
            }catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    基于前面的例子,在和com.lfqy.socket.server.ServerSocket0Test相同包下,创建com.lfqy.socket.server.ServerSocket1Test作为客户端,如下。

    package com.lfqy.socket.server;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    /**
     * Created by chengxia on 2019/9/23.
     */
    public class ServerSocket1Test {
        public static void main(String []args){
            try {
                // 初始化服务端socket并且绑定9999端口
                ServerSocket serverSocket  = new ServerSocket(9999);
                //等待客户端的连接
                Socket socket = serverSocket.accept();
                //获取输入流,并且指定统一的编码格式
                BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
                //通过循环,不断地读取客户端发过来的数据,并输出到控制台
                String str = null;
                while ((str = bufferedReader.readLine()) != null){
                    //输出打印
                    System.out.println("Received: " + str);
                }
            }catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    运行服务器端的程序之后,运行客户端,客户端控制台输入如下:

    Hi, Server!
    Sent: Hi, Server!
    I am a Client.
    Sent: I am a Client.
    

    对应服务器端的控制台输出如下:

    Received: Hi, Server!
    Received: I am a Client.
    

    3.3 同一个客户端处理多个客户端的连接请求_多线程实现

    前面的例子中,一个服务器程序对应一个客户端,同时,只能有一个客户端连接。下面的例子中,我们引入多线程,让一个服务器端可以接受多个客户端的连接。
    原理上比较简单,就是服务器端程序一直在等待连接,每当有客户端连接时,就新建一个线程处理该客户端连接。然后,继续等待下一个客户端程序连接。代码如下。

    package com.lfqy.socket.server;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    /**
     * Created by chengxia on 2019/9/23.
     */
    public class ServerSocket11Test {
        //由于在内部类中用到了这个变量,所以这个变量不能是局部变量,否则编译异常
        private static Socket socket;
        public static void main(String []args){
            try {
                // 初始化服务端socket并且绑定9999端口
                ServerSocket serverSocket  = new ServerSocket(9999);
                while(true){
                    //等待客户端的连接
                    socket = serverSocket.accept();
    
                    //每当有一个客户端连接进来后,就启动一个单独的线程进行处理
                    new Thread(new Runnable() {
                        public void run() {
                            //获取输入流,并且指定统一的编码格式
                            BufferedReader bufferedReader =null;
                            try {
                                bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
                                //读取一行数据
                                String str;
                                //通过while循环不断读取信息,
                                while ((str = bufferedReader.readLine())!=null){
                                    //输出打印
                                    System.out.println("Received: "+str);
                                }
                            }catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }).start();
                }
            }catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    这样,运行com.lfqy.socket.server.ServerSocket11Test启动服务器程序之后,分别运行两次客户端连接程序,并在控制台做如下输入。
    第一次运行com.lfqy.socket.client.ClientSocket1Test

    Hi, server! I am client 1.
    Sent: Hi, server! I am client 1.
    
    

    第二次运行com.lfqy.socket.client.ClientSocket1Test

    Hi, server! I am client 2.
    Sent: Hi, server! I am client 2.
    
    

    这时候,查看服务器程序的控制台输出,发现两个客户端发送的数据都已经收到了。如下。

    Received: Hi, server! I am client 1.
    Received: Hi, server! I am client 2.
    
    

    3.4 线程池实现服务器端同时处理多个客户端的连接

    上面的例子中,每一个连到服务器端的客户端独占一个线程。如果有大量的客户端连接到服务器端,就会有大量的线程被新建出来,而很多线程可能只进行特别少的通信就被闲置了,这样会导致非常大的性能开销,Java的内存回收机制可能不能及时回收这些资源,导致性能浪费。
    为此,我们可以使用线程池技术,来复用线程。在下面的例子中,我们创建了一个大小为100的线程池,线程的创建和回收由线程池管理,这样实现了线程的复用。代码如下。
    com.lfqy.socket.server.ServerSocket12Test

    package com.lfqy.socket.server;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * Created by chengxia on 2019/9/23.
     */
    public class ServerSocket12Test {
        //由于在内部类中用到了这个变量,所以这个变量不能是局部变量,否则编译异常
        private static Socket socket;
        public static void main(String []args){
            try {
                // 初始化服务端socket并且绑定9999端口
                ServerSocket serverSocket  = new ServerSocket(9999);
                //创建一个线程池
                ExecutorService executorService = Executors.newFixedThreadPool(100);
                while(true){
                    //等待客户端的连接
                    socket = serverSocket.accept();
    
                    //每当有一个客户端连接进来后,就启动一个单独的线程进行处理
                    Runnable r = new Runnable() {
                        public void run() {
                            //获取输入流,并且指定统一的编码格式
                            BufferedReader bufferedReader =null;
                            try {
                                bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
                                //读取一行数据
                                String str;
                                //通过while循环不断读取信息,
                                while ((str = bufferedReader.readLine())!=null){
                                    //输出打印
                                    System.out.println("Received: "+str);
                                }
                            }catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    };
                    executorService.submit(r);
                }
            }catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    这样,运行com.lfqy.socket.server.ServerSocket12Test启动服务器端程序。第一次启动com.lfqy.socket.client.ClientSocket1Test,在控制台输入一行发送数据,之后,再运行com.lfqy.socket.client.ClientSocket1Test启动另一个客户端程序,在控制台输入一行数据之后,关掉第一个客户端程序,再在第二个客户端程序控制台输入另外一行数据。最后三个控制台输出如下。
    客户端控制台1:

    Hi, Server! This is Client 1.
    Sent: Hi, Server! This is Client 1.
    
    Process finished with exit code 130 (interrupted by signal 2: SIGINT)
    
    

    客户端控制台2:

    Hi, Server! This is Client 2.
    Sent: Hi, Server! This is Client 2.
    Hi, Client 1 is over. I am Client 2.
    Sent: Hi, Client 1 is over. I am Client 2.
    
    

    服务器端控制台:

    Received: Hi, Server! This is Client 1.
    Received: Hi, Server! This is Client 2.
    Received: Hi, Client 1 is over. I am Client 2.
    
    

    3.5 Socket发送指定长度的信息

    前面的例子中,Socket客户端和服务器端的通信都是以数据行为单位的,每次发送和接收都是一行数据,发送的数据行之间必须有行分隔符。在实际的Socket编程应用中,这样非常不方便,而且有场景限制。通常,我们会指定发送数据的长度,并将数据类型写入到数据的头部信息中,这样再读取时,就可以更加灵活。代码如下。
    com.lfqy.socket.server.ServerSocket2Test

    package com.lfqy.socket.server;
    
    import java.io.*;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    /**
     * Created by chengxia on 2019/9/23.
     */
    public class ServerSocket2Test {
        public static void main(String []args){
            try {
                // 初始化服务端socket并且绑定9999端口
                ServerSocket serverSocket  = new ServerSocket(9999);
                //等待客户端的连接
                Socket socket = serverSocket.accept();
                InputStream inputStream = socket.getInputStream();
                DataInputStream dataInputStream =new DataInputStream(inputStream);
                while (true){
                    //读取数据类型
                    byte b = dataInputStream.readByte();
                    //读取长度
                    int len = dataInputStream.readInt();
                    //因为前五个字节是包头,所以在读取内容的时候,应该去掉前五个字节
                    byte[] data =new byte[len -5];
                    dataInputStream.readFully(data);
                    String str =new String(data);
                    System.out.println("Received type:"+b);
                    System.out.println("Received length:"+len);
                    System.out.println("Received content:"+str);
                }
            }catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    com.lfqy.socket.client.ClientSocket2Test

    package com.lfqy.socket.client;
    
    import java.io.*;
    import java.net.Socket;
    import java.util.Scanner;
    
    /**
     * Created by chengxia on 2019/9/23.
     */
    public class ClientSocket2Test {
        public static void main(String []args){
            try {
                Socket socket =new Socket("127.0.0.1",9999);
                //获得向socket写入数据的输出流
                OutputStream outputStream = socket.getOutputStream();
                DataOutputStream dataOutputStream =new DataOutputStream(outputStream);
                //从控制台接受输入,发送到服务器端
                //通过标准输入流获取字符流
                BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(System.in,"UTF-8"));
    
                while (true){
                    String str = bufferedReader.readLine();
                    //在向服务器发送之前,先加一个包头,包含:类型+长度,共5个字节。
                    byte type =1;
                    byte[] data = str.getBytes();
                    int len = data.length +5;
                    dataOutputStream.writeByte(type);
                    dataOutputStream.writeInt(len);
                    dataOutputStream.write(data);
                    dataOutputStream.flush();
                }
            }catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    在这个例子中,客户端在向服务器端发送数据的时候,向数据包中写入了一个长度为5的头部:第一个字节标识数据类型,后面是一个四个字节的int数据标识数据包的长度。在服务器端读取的时候,只需要先读一个字节,拿到数据类型,然后,再读一个int拿到数据包的长度len,这样,后面数据正文部分的长度应该是len-5,直接将这部分全部督读到一个长度为len-5的byte数组中即可。
    运行com.lfqy.socket.server.ServerSocket2Test启动服务器端程序,运行com.lfqy.socket.client.ClientSocket2Test启动客户端程序,在客户端程序的控制台输入如下:

    Hello!
    
    

    这时候,服务器端的程序输出如下:

    Received type:1
    Received length:11
    Received content:Hello!
    
    

    上面输出的长度11实际上就是Hello!的长度6,加上头部信息长度5的和。

    Structure

    上面的所有例子都做完之后,项目的结构如下:


    Final Structure

    4、Socket长连接和短连接

    长连接指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包。整个通讯过程,客户端和服务端只用一个Socket对象,长期保持Socket的连接。
    短连接是每次请求都建立链接,交互完之后关闭链接。
    长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是短连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,下次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接,如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
    而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好。

    前面的例子都是短连接,每次连接完毕后,都是自动断开,如果需要重新连接,则需要建立新的连接对象。在实际应用中,长连接他并不是真正意义上的长连接,(他不像我们打电话一样,电话通了之后一直不挂的这种连接)。他们是通过一种称之为心跳包或者叫做链路检测包,去定时检查socket是否关闭,输入输出流是否关闭。
    socket是通过流的方式通信的,既然关闭流,就是关闭socket,那么长连接是不是我们读取流中的信息后,不关闭流,等下次使用时,直接往流中扔数据就?
    不是这样的,socket是针对应用层与TCP/ip数据传输协议封装的一套方案,那么他的底层也是通过Tcp/Tcp/ip或则UDP通信的,所以说socket本身并不是一直通信协议,而是一套接口的封装。而tcp/IP协议组里面的应用层包括FTP、HTTP、TELNET、SMTP、DNS等协议,我们知道,http1.0是短连接,http1.1是长连接,我们在打开http通信协议里面在Response headers中可以看到这么一句Connection:keep-alive,用来表示表示长连接。但是长连接并非一直保持的连接,它在制定的时间内让客户端和服务端进行一个请求来保持该连接,请求可以是服务端发起,也可以是客户端发起。通常在客户端不定时的发送一个字节数据给服务端,称之为心跳包。

    参考资料

    相关文章

      网友评论

        本文标题:网络协议、端口和Socket

        本文链接:https://www.haomeiwen.com/subject/iqeouctx.html