美文网首页杂七杂八我爱编程好文收藏
Java socket详解,看这一篇就够了

Java socket详解,看这一篇就够了

作者: 长道 | 来源:发表于2018-04-02 15:58 被阅读528次

            刚给大家讲解Java socket通信后,好多童鞋私信我,有好多地方不理解,看不明白。特抽时间整理一下,详细讲述Java socket通信原理和实现案例。整个过程楼主都是通过先简单明了的示例让大家了解整个基本原理,后慢慢接近生产实用示例,先概况后脉络给大家梳理出来的,所有涉及示例都可以直接拷贝运行。楼主才疏学浅,如有部分原理错误请大家及时指正,或发邮件与楼主交流:mail:twisttime_8633@aliyun.com. QQ:125717901。

    请尊重作者劳动成果,转载请标明原文链接:https://www.jianshu.com/p/cde27461c226

            整理和总结了一下大家常遇到的问题:

           1.    客户端socket发送消息后,为什么服务端socket没有收到?

            2.    使用while 循环实现连续输入,是不是就是多线程模式?

            3.    对多线程处理机制不是很明白,希望详细讲解?

            4.    希望详细讲解ServerSocketChannel和SocketChannel与ServerSoket和Socket的区别?

            5.    希望有详细的例子,可以直接拷贝下来运行?

    针对童鞋们提出的问题,我会在本文章中详细一一简答,并且给出详细的例子,下面言归正传。

    一:socket通信基本原理。

    首先socket 通信是基于TCP/IP 网络层上的一种传送方式,我们通常把TCP和UDP称为传输层。

    如上图,在七个层级关系中,我们将的socket属于传输层,其中UDP是一种面向无连接的传输层协议。UDP不关心对端是否真正收到了传送过去的数据。如果需要检查对端是否收到分组数据包,或者对端是否连接到网络,则需要在应用程序中实现。UDP常用在分组数据较少或多播、广播通信以及视频通信等多媒体领域。在这里我们不进行详细讨论,这里主要讲解的是基于TCP/IP协议下的socket通信。

    socket是基于应用服务与TCP/IP通信之间的一个抽象,他将TCP/IP协议里面复杂的通信逻辑进行分装,对用户来说,只要通过一组简单的API就可以实现网络的连接。借用网络上一组socket通信图给大家进行详细讲解:

    首先,服务端初始化ServerSocket,然后对指定的端口进行绑定,接着对端口及进行监听,通过调用accept方法阻塞,此时,如果客户端有一个socket连接到服务端,那么服务端通过监听和accept方法可以与客户端进行连接。

    二:socket通信基本示例:

    在对socket通信基本原理明白后,那我们就写一个最简单的示例,展示童鞋们常遇到的第一个问题:客户端发送消息后,服务端无法收到消息。

    服务端:

    package socket.socket1.socket;

    import java.io.BufferedReader;

    import java.io.BufferedWriter;

    import java.io.IOException;

    import java.io.InputStreamReader;

    import java.net.ServerSocket;

    import java.net.Socket;

    public class ServerSocketTest {

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

            }

    }

    }

    客户端:

    package socket.socket1.socket;

    import java.io.BufferedWriter;

    import java.io.IOException;

    import java.io.OutputStreamWriter;

    import java.net.Socket;

    public class ClientSocket {

    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="你好,这是我的第一个socket";

                bufferedWriter.write(str);

            }catch (IOException e) {

    e.printStackTrace();

            }

    }

    }

    启动服务端:

    发现正常,等待客户端的的连接

    启动客户端:

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

    服务端控制台报错:

    然后好多童鞋,就拷贝这个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()是将输出流关闭,此时,如果服务端有信息返回,则客户端是可以正常接受的。现在我们将上面的客户端示例修改一下啊,增加一个标识告诉流已经输出完毕:

    客户端2:

    package socket.socket1.socket;

    import java.io.BufferedWriter;

    import java.io.IOException;

    import java.io.OutputStreamWriter;

    import java.net.Socket;

    public class ClientSocket {

    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="你好,这是我的第一个socket";

                bufferedWriter.write(str);

                //刷新输入流

                bufferedWriter.flush();

                //关闭socket的输出流

                socket.shutdownOutput();

            }catch (IOException e) {

    e.printStackTrace();

            }

    }

    }

    在看服务端控制台:

    服务端在接受到客户端关闭流的信息后,知道信息输入已经完毕,苏哦有就能正常读取到客户端传过来的数据。通过上面示例,我们可以基本了解socket通信原理,掌握了一些socket通信的基本api和方法,实际应用中,都是通过此处进行实现变通的。

    三:while循环连续接受客户端信息:

    上面的示例中scoket客户端和服务端固然可以通信,但是客户端每次发送信息后socket就需要关闭,下次如果需要发送信息,需要socket从新启动,这显然是无法适应生产环境的需要。比如在我们是实际应用中QQ,如果每次发送一条信息,就需要重新登陆QQ,我估计这程序不是给人设计的,那么如何让服务可以连续给服务端发送消息?下面我们通过while循环进行简单展示:

    服务端:

    package socket.socket1.socket;

    import java.io.BufferedReader;

    import java.io.BufferedWriter;

    import java.io.IOException;

    import java.io.InputStreamReader;

    import java.net.ServerSocket;

    import java.net.Socket;

    public class ServerSocketTest {

    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;

                //通过while循环不断读取信息,

                while ((str = bufferedReader.readLine())!=null){

    //输出打印

                    System.out.println(str);

                }

    }catch (IOException e) {

    e.printStackTrace();

            }

    }

    }

    客户端:

    package socket.socket1.socket;

    import java.io.*;

    import java.net.Socket;

    public class ClientSocket {

    public static void main(String[] args) {

    try {

    //初始化一个socket

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

              }

    }catch (IOException e) {

    e.printStackTrace();

            }

    }

    }

    客户端控制中心:

    服务端控制中心:

    大家可以看到,通过一个while 循环,就可以实现客户端不间断的通过标准输入流读取来的消息,发送给服务端。在这里有个细节,大家看到没有,我客户端没有写socket.close() 或者调用socket.shutdownOutput();服务端是如何知道客户端已经输入完成了?服务端接受数据的时候是如何判断客户端已经输入完成呢?这就是一个核心点,双方约定一个标识,当客户端发送一个标识给服务端时,表明客户端端已经完成一个数据的载入。而服务端在结束数据的时候,也通过这个标识进行判断,如果接受到这个标识,表明数据已经传入完成,那么服务端就可以将数据度入后显示出来。

            在上面的示例中,客户端端在循环发送数据时候,每发送一行,添加一个换行标识“\n”标识,在告诉服务端我数据已经发送完成了。而服务端在读取客户数据时,通过while ((str = bufferedReader.readLine())!=null)去判断是否读到了流的结尾,负责服务端将会一直阻塞在哪里,等待客户端的输入。

            通过while方式,我们可以实现多个客户端和服务端进行聊天。但是,下面敲黑板,划重点。由于socket通信是阻塞式的,假设我现在有A和B俩个客户端同时连接到服务端的上,当客户端A发送信息给服务端后,那么服务端将一直阻塞在A的客户端上,不同的通过while循环从A客户端读取信息,此时如果B给服务端发送信息时,将进入阻塞队列,直到A客户端发送完毕,并且退出后,B才可以和服务端进行通信。简单地说,我们现在实现的功能,虽然可以让客户端不间断的和服务端进行通信,与其说是一对一的功能,因为只有当客户端A关闭后,客户端B才可以真正和服务端进行通信,这显然不是我们想要的。 下面我们通过多线程的方式给大家实现正常人类的思维。

    四:多线程下socket编程

    服务端:

    package socket.socket1.socket;

    import java.io.BufferedReader;

    import java.io.BufferedWriter;

    import java.io.IOException;

    import java.io.InputStreamReader;

    import java.net.ServerSocket;

    import java.net.Socket;

    public class ServerSocketTest {

    public static void main(String[] args)throws IOException {

    // 初始化服务端socket并且绑定9999端口

                ServerSocket serverSocket  =new ServerSocket(9999);

                while (true){

    //等待客户端的连接

                    Socket socket = serverSocket.accept();

                    //每当有一个客户端连接进来后,就启动一个单独的线程进行处理

                    new Thread(new Runnable() {

    @Override

                        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("客户端说:"+str);

                                }

    }catch (IOException e) {

    e.printStackTrace();

                            }

    }

    }).start();

                }

    }

    }

    客户端:

    package socket.socket1.socket;

    import java.io.*;

    import java.net.Socket;

    public class ClientSocket {

    public static void main(String[] args) {

    try {

    //初始化一个socket

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

              }

    }catch (IOException e) {

    e.printStackTrace();

            }

    }

    }

    通过客户端A控制台输入:

    通过客户端B控制台输入:

    服务端控制台:

    通过这里我们可以发现,客户端A和客户端B同时连接到服务端后,都可以和服务端进行通信,也不会出现前面讲到使用while(true)时候客户端A连接时客户端B不能与服务端进行交互的情况。在这里我们看到,主要是通过服务端的 new Thread(new Runnable() {}实现的,每一个客户端连接进来后,服务端都会单独起个一线程,与客户端进行数据交互,这样就保证了每个客户端处理的数据是单独的,不会出现相互阻塞的情况,这样就基本是实现了QQ程序的基本聊天原理。

            但是实际生产环境中,这种写法对于客户端连接少的的情况下是没有问题,但是如果有大批量的客户端连接进行,那我们服务端估计就要歇菜了。假如有上万个socket连接进来,服务端就是新建这么多进程,反正楼主是不敢想,而且socket 的回收机制又不是很及时,这么多线程被new 出来,就发送一句话,然后就没有然后了,导致服务端被大量的无用线程暂用,对性能是非常大的消耗,在实际生产过程中,我们可以通过线程池技术,保证线程的复用,下面请看改良后的服务端程序。

    改良后的服务端:

    package socket.socket1.socket;

    import java.beans.Encoder;

    import java.io.BufferedReader;

    import java.io.BufferedWriter;

    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;

    public class ServerSocketTest {

    public static void main(String[] args)throws IOException {

    // 初始化服务端socket并且绑定9999端口

            ServerSocket serverSocket =new ServerSocket(9999);

            //创建一个线程池

            ExecutorService executorService = Executors.newFixedThreadPool(100);

            while (true) {

    //等待客户端的连接

                Socket socket = serverSocket.accept();

                Runnable runnable = () -> {

    BufferedReader bufferedReader =null;

                    try {

    bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));

                        //读取一行数据

                        String str;

                        //通过while循环不断读取信息,

                        while ((str = bufferedReader.readLine()) !=null) {

    //输出打印

                            System.out.println("客户端说:" + str);

                        }

    }catch (IOException e) {

    e.printStackTrace();

                    }

    };

                executorService.submit(runnable);

            }

    }

    }

    运行后服务端控制台:

    通过线程池技术,我们可以实现线程的复用。其实在这里executorService.submit在并发时,如果要求当前执行完毕的线程有返回结果时,这里面有一个大坑,在这里我就不一一详细说明,具体我在我的另一篇文章中《把多线程说个透》里面详细介绍。本章主要讲述socket相关内容。

    在实际应用中,socket发送的数据并不是按照一行一行发送的,比如我们常见的报文,那么我们就不能要求每发送一次数据,都在增加一个“\n”标识,这是及其不专业的,在实际应用中,通过是采用数据长度+类型+数据的方式,在我们常接触的热Redis就是采用这种方式,

    五:socket 指定长度发送数据

    清明后跟新。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

    六:socket 建立长连接

    清明后跟新。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

    七:非阻塞ServerSocketChannel通信

    清明后跟新。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

    八:socket服务端接受信息后反馈给客户端

    清明后跟新。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

    九:socket经典小例子

    清明后跟新。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

    由于节前工作比较繁忙,节后会将后续章节进行详细跟新。

    相关文章

      网友评论

      本文标题:Java socket详解,看这一篇就够了

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