JAVA SOCKET 详解

概述

本人在开发学习NETTY的过程中,需要了解很多的网络开发知识,在此我总结一些关于socket的基础知识,大部分是网络总结,在此篇的随笔中记录socket的知识,以便于记录,如有问题欢迎大家斧正。

SOCKET通信基本原理

首先socket通常也叫做“套接字”,用于描述IP地址和端口,是一个通信连的句柄。应用程序通常通过“套接字”向网络发出请求或者应答网络请求。 socket通信是基于TCP/IP网络层上的一种传输方式,我们通常把TCP和UDP 称为传输层。

如上图所示,在七个层级关系中,我们将socket层属于传输层,其中UDP是一种面向无连接的传输层协议。UDP不关系对端是否真正收到了数据,如果需要检查需要在应用程序中实现。这里不详细讨论。

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

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

 

SOCKET通信基本示例

在对socket通信基本原理明白后,那我们就写一个最简单的示例,

服务端

package com.chinalife.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);

        }
    }
}

客户端

package com.chinalife.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("
");
                bufferedWriter.flush();
            }
        }catch (IOException e) {e.printStackTrace();
        }
    }
}

需要注意的地方如下:

一,socket通信是阻塞的,他会在以下几个地方进行阻塞。第一个是accept方法,调用这个方法后,服务端一直阻塞在哪里,直到有客户端连接进来。第二个是read方法,调用read方法也会进行阻塞,客户端发送完消息后,需要给服务端一个标识,告诉服务端,我已经发送完成了,服务端就可以将接受的消息打印出来。socket.close() 或者调用socket.shutdownOutput();方法。调用这俩个方法,都会结束客户端socket。但是有本质的区别。socket.close() 将socket关闭连接,那边如果有服务端给客户端反馈信息,此时客户端是收不到的。而socket.shutdownOutput()是将输出流关闭,此时,如果服务端有信息返回,则客户端是可以正常接受的。

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

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

SOCKET指定长度发送数据

在实际应用中,网络的数据在TCP/IP协议下的socket都是采用数据流的方式进行发送,那么在发送过程中就要求我们将数据流转出字节进行发送,读取的过程中也是采用字节缓存的方式结束。那么问题就来了,在socket通信时候,我们大多数发送的数据都是不定长的,所有接受方也不知道此次数据发送有多长,因此无法精确地创建一个缓冲区(字节数组)用来接收,在不定长通讯中,通常使用的方式时每次默认读取8*1024长度的字节,若输入流中仍有数据,则再次读取,一直到输入流没有数据为止。但是如果发送数据过大时,发送方会对数据进行分包发送,这种情况下或导致接收方判断错误,误以为数据传输完成,因而接收不全。在这种情况下就会引出一些问题,诸如半包,粘包,分包等问题,为了后续一些例子中好理解,我在这里直接将半包,粘包,分包概念性东西在写一下(引用度娘)。
半包:

接受方没有接受到一个完整的包,只接受了部分。

原因:TCP为提高传输效率,将一个包分配的足够大,导致接受方并不能一次接受完。

影响:长连接和短连接中都会出现

粘包:

发送方发送的多个包数据到接收方接收时粘成一个包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

分类:一种是粘在一起的包都是完整的数据包,另一种情况是粘在一起的包有不完整的包

出现粘包现象的原因是多方面的:

1)发送方粘包:由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。

2)接收方粘包:接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。

分包:

分包(1):在出现粘包的时候,我们的接收方要进行分包处理;

分包(2):一个数据包被分成了多次接收;

原因:1. IP分片传输导致的;2.传输过程中丢失部分包导致出现的半包;3.一个包可能被分成了两次传输,在取数据的时候,先取到了一部分(还可能与接收的缓冲区大小有关系)。

影响:粘包和分包在长连接中都会出现

那么如何解决半包和粘包的问题,就涉及一个一个数据发送如何标识结束的问题,通常有以下几种情况

固定长度:每次发送固定长度的数据;

特殊标示:以回车,换行作为特殊标示;获取到指定的标识时,说明包获取完整。

字节长度:包头+包长+包体的协议形式,当服务器端获取到指定的包长时才说明获取完整;

所以大部分情况下,双方使用socket通讯时都会约定一个定长头放在传输数据的最前端,用以标识数据体的长度,通常定长头有整型int,短整型short,字符串Strinng三种形式。

SOCKET建立长连接

长连接指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包。整个通讯过程,客户端和服务端只用一个Socket对象,长期保持Socket的连接。

短连接短连接服务是每次请求都建立链接,交互完之后关闭链接。

长连接和短连接的优势

长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是短连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,下次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接,如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费

而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好。

首先我们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。他是干什么的,他就是表示长连接,但是他并不是一直保持的连接,他有一个时间段,如果我们想一直保持这个连接怎么办?那就是在制定的时间内让客户端和服务端进行一个请求,请求可以是服务端发起,也可以是客户端发起,通常我们是在客户端不定时的发送一个字节数据给服务端,这个就是我们称之为心跳包,想想心跳是怎么跳动的,是不是为了检测人活着,心会定时的跳动,就是这个原理。
 

总结

网络通信连接知识很多这里只是借鉴了网上一片文章,链接:https://www.jianshu.com/p/cde27461c226

对那些免费提供材料的网络IT大神致以敬意。

 
 
 
 
原文地址:https://www.cnblogs.com/boanxin/p/11369994.html