zoukankan      html  css  js  c++  java
  • java Socket

    TCP/IP 协议简介

    IP

    首先我们看 IP(Internet Protocol)协议。IP 协议提供了主机和主机间的通信。

    为了完成不同主机的通信,我们需要某种方式来唯一标识一台主机,这个标识,就是著名的IP地址。通过IP地址,IP 协议就能够帮我们把一个数据包发送给对方。


    TCP

    前面我们说过,IP 协议提供了主机和主机间的通信。
    TCP 协议在 IP 协议提供的主机间通信功能的基础上,完成这两个主机上进程对进程的通信。

    有了 IP,不同主机就能够交换数据。但是,计算机收到数据后,并不知道这个数据属于哪个进程(简单讲,进程就是一个正在运行的应用程序)。TCP 的作用就在于,让我们能够知道这个数据属于哪个进程,从而完成进程间的通信。

    为了标识数据属于哪个进程,我们给需要进行 TCP 通信的进程分配一个唯一的数字来标识它。这个数字,就是我们常说的端口号

    三次握手

    TCP 的全称是 Transmission Control Protocol,大家对它说得最多的,大概就是面向连接的特性了。之所以说它是有连接的,是说在进行通信前,通信双方需要先经过一个三次握手的过程。三次握手完成后,连接便建立了。这时候我们才可以开始发送/接收数据。(与之相对的是 UDP,不需要经过握手,就可以直接发送数据)。

    下面我们简单了解一下三次握手的过程。

    1. 首先,客户向服务端发送一个 SYN,假设此时 sequence number 为 x。这个 x 是由操作系统根据一定的规则生成的,不妨认为它是一个随机数。
    2. 服务端收到 SYN 后,会向客户端再发送一个 SYN,此时服务器的 seq number = y。与此同时,会 ACK x+1,告诉客户端“已经收到了 SYN,可以发送数据了”。
    3. 客户端收到服务器的 SYN 后,回复一个 ACK y+1,这个 ACK 则是告诉服务器,SYN 已经收到,服务器可以发送数据了。

    经过这 3 步,TCP 连接就建立了。这里需要注意的有三点:

    1. 连接是由客户端主动发起的
    2. 在第 3 步客户端向服务器回复 ACK 的时候,TCP 协议是允许我们携带数据的。之所以做不到,是 API 的限制导致的。
    3. TCP 协议还允许 “四次握手” 的发生,同样的,由于 API 的限制,这个极端的情况并不会发生

    一、socket通信基本原理

    Socket 是 TCP 层的封装,通过 socket,我们就能进行 TCP 通信。

    socket 通信是基于TCP/IP协议的一种传送方式,实现网络间的双向通信,我们通常把TCP和UDP称为传输层。

    如上图,在七个层级关系中,我们讲的socket属于传输层,

    其中UDP是一种面向无连接的传输层协议。UDP不关心对端是否真正收到了传送过去的数据。

    如果需要检查对端是否收到分组数据包,或者对端是否连接到网络,则需要在应用程序中实现。

    UDP常用在分组数据较少或多播、广播通信以及视频通信等多媒体领域。

    在这里我们不进行详细讨论,这里主要讲解的是基于TCP/IP协议下的socket通信。

    socket是基于应用服务与TCP/IP通信之间的一个抽象,他将TCP/IP协议里面复杂的通信逻辑进行分装,

    对用户来说,只要通过一组简单的API就可以实现网络的连接

    首先,服务端初始化ServerSocket,然后对指定的端口进行绑定,接着对端口及进行监听,通过调用accept方法阻塞,

    此时,如果客户端有一个socket连接到服务端,那么服务端通过监听和accept方法可以与客户端进行连接。

    二  基本示例 

    服务端

     1 package socket.socket1.socket;
     2 
     3 import java.io.BufferedReader;
     4 import java.io.IOException;
     5 import java.io.InputStreamReader;
     6 import java.net.ServerSocket;
     7 import java.net.Socket;
     8 
     9 public class ServerSocketTest {
    10 
    11     public static void main(String[] args) {
    12         try {
    13             //初始化服务端socket并且绑定9999端口
    14             ServerSocket serverSocket = new ServerSocket(9999);
    15             //等待客户端的连接
    16             Socket socket = serverSocket.accept();
    17             //获取输入流
    18             BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    19             //读取一行数据
    20             String str = bufferedReader.readLine();
    21             //输出打印
    22             System.out.println(str);
    23         } catch (IOException e) {
    24             e.printStackTrace();
    25         }
    26     }
    27 }

    客户端

     1 package socket.socket1.socket;
     2 
     3 import java.io.BufferedWriter;
     4 import java.io.IOException;
     5 import java.io.OutputStreamWriter;
     6 import java.net.Socket;
     7 
     8 public class ClientSocket {
     9     public static void main(String[] args) {
    10         try {
    11             Socket socket = new Socket("127.0.0.1", 9999);
    12             BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
    13             String str = "你好,这是我的第一个socket";
    14             bufferedWriter.write(str);
    15         } catch (IOException e) {
    16             e.printStackTrace();
    17         }
    18     }
    19 }

    先启动服务端

    再启动客户端

    发现客户端启动正常后,马上执行完后关闭。同时服务端控制台报错:

    服务端报错

    然后好多童鞋,就拷贝这个java.net.SocketException: Connection reset上王查异常,查询解决方案,搞了半天都不知道怎么回事。

    解决这个问题我们首先要明白,socket通信是阻塞的,他会在以下几个地方进行阻塞。

    第一个是accept方法,调用这个方法后,服务端一直阻塞在哪里,直到有客户端连接进来。

    第二个是read方法,调用read方法也会进行阻塞。通过上面的示例我们可以发现,该问题发生在read方法中。

    有朋友说是Client没有发送成功,其实不是的,我们可以通debug跟踪一下,发现客户端发送了,并且没有问题。

    而是发生在服务端中,当服务端调用read方法后,他一直阻塞在哪里,因为客户端没有给他一个标识,告诉是否消息发送完成,

    所以服务端还在一直等待接受客户端的数据,结果客户端此时已经关闭了,就是在服务端报错:java.net.SocketException: Connection reset

    那么理解上面的原理后,我们就能明白,客户端发送完消息后,需要给服务端一个标识,告诉服务端,我已经发送完成了,服务端就可以将接受的消息打印出来。

    通常大家会用以下方法进行进行结束:

    调用socket.close() 或者socket.shutdownOutput()方法。

    调用这俩个方法,都会结束客户端socket。但是有本质的区别。

    socket.close() 将socket关闭连接,那边如果有服务端给客户端反馈信息,此时客户端是收不到的。

    socket.shutdownOutput()是将输出流关闭,此时,如果服务端有信息返回,则客户端是可以正常接受的。

    现在我们将上面的客户端示例修改一下啊,增加一个标识告诉流已经输出完毕:

    客户端

     1 package socket.socket1.socket;
     2 
     3 import java.io.BufferedWriter;
     4 import java.io.IOException;
     5 import java.io.OutputStreamWriter;
     6 import java.net.Socket;
     7 
     8 public class ClientSocket {
     9     public static void main(String[] args) {
    10         try {
    11             Socket socket = new Socket("127.0.0.1", 9999);
    12             BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
    13             String str = "你好,这是我的第一个socket";
    14             bufferedWriter.write(str);
    15             //刷新输入流
    16             bufferedWriter.flush();
    17             //关闭socket的输出流
    18             socket.shutdownOutput();
    19         } catch (IOException e) {
    20             e.printStackTrace();
    21         }
    22     }
    23 }

    在看服务端控制台:

    通过上面示例,我们可以基本了解socket通信原理,掌握了一些socket通信的基本api和方法,实际应用中,都是通过此处进行实现变通的。

    但上面示例,其实不够完整,比如我们每次发送都要new 一个socket ,也只支持一次发送消息,所以我们用另外一个例子,实现1个比较完整的demo

    三  手写完整示例

    例用Socket实现客户端和服务端通信,要求客户发送数据后回显相同的数据

     服务端 socket 

    package com.differ.jackyun.examples.javabasisc.socket;
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * 服务端soccket 测试
     *
     * @author hup
     * @data 2020-05-31 14:30
     **/
    public class MyServerSocket implements Runnable {
    
        @Override
        public void run() {
            //创建一个线程池
            ExecutorService executorService = Executors.newFixedThreadPool(100);
            try {
                ServerSocket server = new ServerSocket(10001);
                while(true)
                {
                    //阻塞等待
                    Socket socket = server.accept();
                    //为了支持并发,所以每来1次消息,都弄个新线程处理
                    Runnable runnable = () -> {
                        //字符输入流
                        BufferedReader reader = null;
                        //字符输出流
                        PrintWriter pw = null;
                        try {
                            //读取接收到的内容
                            reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                            //接收到的数据
                            String readResult = reader.readLine();
                            System.out.println("服务端接收到数据=" + readResult);
                            //数据发回客户端
                            pw = new PrintWriter(socket.getOutputStream(), true);
                            pw.println(readResult);
                        } catch (Exception e) {
                        } finally {
                            //关闭流
                            try {
                                if (reader != null) {
                                    reader.close();
                                }
                                if (pw != null) {
                                    pw.close();
                                }
                            } catch (Exception e) {
                            }
                        }
                    };
                    //线程池提交线程任务
                    executorService.submit(runnable);
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }

    客户端 socket

    package com.differ.jackyun.examples.javabasisc.socket;
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.Socket;
    
    /**
     * 客户端socket
     *
     * @author hup
     * @data 2020-05-31 14:30
     **/
    public class MySocket implements Runnable {
    
        @Override
        public void run() {
            //输出字符流
            PrintWriter pw = null;
            //输入字符流
            BufferedReader reader = null;
            try {
                //输出字符流
                Socket socket = new Socket("localhost", 10001);
                pw = new PrintWriter(socket.getOutputStream(), true);
                //向服务端发送消息
                pw.println("我是客户端消息,今天天气真好");
                //等待服务器端的消息
                reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
                while (true) {
                    String result = reader.readLine();
                    if (result != null) {
                        System.out.println("客户端接收到服务端消息=" + result);
                        break;
                    }
                }
            } catch (Exception ex) {
            } finally {
                //关闭流
                try {
                    if (pw != null) {
                        pw.close();
                    }
                    if (reader != null) {
                        reader.close();
                    }
                } catch (Exception e) {
                }
            }
        }
    }

    测试类

     1 package com.differ.jackyun.examples.javabasisc.socket;
     2 
     3 import org.junit.Test;
     4 
     5 /**
     6  * 套接字测试类
     7  *
     8  * @author hup
     9  * @data 2020-05-31 15:14
    10  **/
    11 public class socketTest {
    12     @Test
    13     public void test() {
    14         //启动服务端
    15         MyServerSocket myServerSocket = new MyServerSocket();
    16         new Thread(myServerSocket).start();
    17 
    18         try {
    19             Thread.currentThread().sleep(5000);
    20         } catch (Exception ex) {
    21             System.out.println(ex);
    22         }
    23 
    24         //启动客户端1
    25         MySocket mySocket = new MySocket();
    26         new Thread(mySocket).start();
    27 
    28         //启动客户端2
    29         MySocket mySocket2 = new MySocket();
    30         new Thread(mySocket2).start();
    31 
    32         try {
    33             Thread.currentThread().sleep(10000);
    34         } catch (Exception ex) {
    35             System.out.println(ex);
    36         }
    38     }
    40 }

    测试输出结果

    服务端接收到数据=我是客户端消息,今天天气真好
    客户端接收到服务端消息=我是客户端消息,今天天气真好
    服务端接收到数据=我是客户端消息,今天天气真好
    客户端接收到服务端消息=我是客户端消息,今天天气真好

    根据结果可以知道: 多个客户端给服务端发消息,服务端都能处理(用到了多线程)

    四   看完上面例子,可能有同学有疑问了,为什么你输入流(读取)的时候用的是BufferedReader, 输出流(写)的时候用的是PrintWriter 不应该用与BufferedReader 配套的BufferedWriter吗?

               Socket编程中,尽量用PrintWriter取代BufferedWriter,下面是PrintWriter的优点:

    1. PrintWriter的print、println方法可以接受任意类型的参数,而BufferedWriter的write方法只能接受字符、字符数组和字符串;

    2. PrintWriter的println方法自动添加换行,BufferedWriter需要显示调用newLine方法;

    3. PrintWriter的方法不会抛异常,若关心异常,需要调用checkError方法看是否有异常发生;

    4. PrintWriter构造方法可指定参数,实现自动刷新缓存(autoflush);

    5. PrintWriter的构造方法更广。

            在使用BufferedReader中的readLine方法接收BufferedWriter中的字符流时,由于readLine是在读取到换行符的时候才将整行字符返回,所以BufferedWriter方法在录入一段字符后要使用newLine方法进行一次换行操作,然后再把字符流刷出去。而PrintWriter由于可以开启自动刷新,并且其中的println方法自带换行操作。所以代码实现起来要比BufferedWriter简单一些。
    ————————————————
    版权声明:最后面这部分总结 来源于下面链接
    原文链接:https://blog.csdn.net/arno_dzl/java/article/details/76601852

  • 相关阅读:
    用例图解析
    A B C D类网络地址
    B-树特征
    常用的中间代码
    UML图
    关于文件索引的一道习题
    数据流图的一些原则
    系统总线
    各种排序方法的时间复杂度、空间复杂度和稳定性统计表
    模拟银行自助终端系统
  • 原文地址:https://www.cnblogs.com/hup666/p/13019464.html
Copyright © 2011-2022 走看看