zoukankan      html  css  js  c++  java
  • Socket的使用

    要运行下面的所有项目,请关闭防火墙

    基于TCP/IP协议的Socket使用

    android 客户端向服务端发送信息

    服务端代码:

    package com.example.admin.server;
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.InetAddress;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.net.UnknownHostException;

    public class MyClass {
    public static void main(String[] arugs) throws IOException {
    try {
    call();
    } catch (IOException e) {
    System.out.print("io failed"); }

    }

    public static void call() throws IOException {
    InputStream inputStream = null;
    Socket socket=null;
    ServerSocket serverSocket=null;

    try {
    serverSocket = new ServerSocket(5050);
    InetAddress address = InetAddress.getLocalHost();
    String ip = address.getHostAddress();
    System.out.println("服务端ip地址:" + ip);
    socket = serverSocket.accept();
    inputStream = socket.getInputStream();
    StringBuilder builder = new StringBuilder();
    byte[] bytes = new byte[1024];
    int length;
    while ((length = inputStream.read(bytes)) != -1) {
    builder.append(new String(bytes, 0, length, "UTF-8"));
    }
    System.out.println(builder.toString());
    } catch (UnknownHostException e) {
    System.out.print("null localHost");
    } finally {
    if (inputStream != null) {
    inputStream.close();
    }if (socket!=null){
    socket.close();
    }if (serverSocket!=null){
    serverSocket.close(); }

    }
    }
    }

    控制台输出: 服务端ip地址:xxx.xxx.x.x


    客户端代码:
    首先添加网络权限
    <uses-permission android:name="android.permission.INTERNET"/>

    package com.example.admin.socket;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.net.Socket;
    import java.net.UnknownHostException;


    public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Button button=findViewById(R.id.button);
    button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    new Thread() {
    @Override
    public void run() {
    super.run();
    try {
    call();
    }catch (IOException e){
    Log.e("io","io close failed"); }

    }
    }.start();

    }
    });
    }

    public void call()throws IOException{
    Socket socket=null;
    OutputStream out=null;

    try {
    socket = new Socket("xxx.xxx.x.x", 5050);
    out = socket.getOutputStream();
    out.write("Hello World".getBytes( "UTF-8"));
    }catch (UnknownHostException e){
    Log.e("socket", "create socket failed");
    } catch (IOException e) {
    Log.e("io","outputstream problem ");

    }finally {
    if (out!=null)
    out.close();
    }
    if (socket!=null){
    socket.close();
    }
    }
    }


    点击按钮后,控制台输出:服务端ip地址:xxx.xxx.x.x
    Hello World




    android客户端向服务端发送消息,并接受消息

    服务端代码:

    package com.example.admin.server;

    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.net.InetAddress;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.net.UnknownHostException;

    public class MyClass {
    public static void main(String[] arugs) throws IOException {
    try {
    call();
    } catch (IOException e) {
    System.out.print("io failed"); }

    }

    public static void call() throws IOException {
    InputStream inputStream = null;
    OutputStream outputStream=null;
    Socket socket=null;
    ServerSocket serverSocket=null;

    try {
    serverSocket = new ServerSocket(5050);
    InetAddress address = InetAddress.getLocalHost();
    String ip = address.getHostAddress();
    System.out.println("服务端ip地址:" + ip);
    socket = serverSocket.accept();
    inputStream = socket.getInputStream();
    StringBuilder builder = new StringBuilder();
    byte[] bytes = new byte[1024];
    int length;
    while ((length = inputStream.read(bytes)) != -1) {
    builder.append(new String(bytes, 0, length, "UTF-8"));
    }
    System.out.println(builder.toString());
    outputStream=socket.getOutputStream();
    outputStream.write("Welcome to the new world!".getBytes("UTF-8"));
    } catch (UnknownHostException e) {
    System.out.print("null localHost");
    } finally {
    if (inputStream != null) {
    inputStream.close();
    }if (outputStream!=null){
    outputStream.close();
    }
    if (socket!=null){
    socket.close();
    }if (serverSocket!=null){
    serverSocket.close();
    }

    }
    }
    }
    客户端代码:
    package com.example.admin.socket;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.TextView;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.net.Socket;
    import java.net.UnknownHostException;

    public class MainActivity extends AppCompatActivity {
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    textView=findViewById(R.id.textview);
    Button button=findViewById(R.id.button);
    button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    new Thread() {
    @Override
    public void run() {
    super.run();
    try {
    call();
    }catch (IOException e){
    Log.e("io","io close failed");
    }

    }
    }.start();

    }

    });

    }



    public void call()throws IOException{
    final StringBuilder builder=new StringBuilder();
    Socket socket=null;
    OutputStream out=null;
    InputStream in=null;

    try {
    socket = new Socket("xxx.xxx.x.x", 5050);
    out = socket.getOutputStream();
    out.write("Hello World".getBytes( "UTF-8"));
    //通过shutdownOutput高速服务器已经发送完数据,后续只能接受数据
    socket.shutdownOutput();
    in=socket.getInputStream();
    byte[] bytes=new byte[1024];
    int length;
    while ((length=in.read(bytes))!=-1){
    builder.append(new String(bytes,0,length,"UTF-8"));
    }
    runOnUiThread(new Runnable() {
    @Override
    public void run() {
    textView.setText(builder.toString());

    }
    });
    }catch (UnknownHostException e){
    Log.e("socket", "create socket failed");
    } catch (IOException e) {
    Log.e("io","outputstream problem ");

    }finally {
    if (out!=null){
    out.close();
    }if (in!=null){
    in.close();
    }
    if (socket!=null){
    socket.close();
    }
    }

    }


    }
    android客户端显示 :Welcome to the new world!

    如何告知对方已发送完命令

      其实这个问题还是比较重要的,正常来说,客户端打开一个输出流,如果不做约定,也不关闭它,那么服务端永远不知道客户端是否发送完消息,那么服务端会一直等待下去,直到读取超时。所以怎么告知服务端已经发送完消息就显得特别重要。

    1.通过Socket关闭

       当Socket关闭的时候,服务端就会收到响应的关闭信号,那么服务端也就知道流已经关闭了,这个时候读取操作完成,就可以继续后续工作。

         但是这种方式有一些缺点

    • 客户端Socket关闭后,将不能接受服务端发送的消息,也不能再次发送消息
    • 如果客户端想再次发送消息,需要重现创建Socket连接

     

    2.通过Socket关闭输出流的方式

      调用Socket的shutdownOutput()方法,底层会告知服务端我这边已经写完了,那么服务端收到消息后,就能知道已经读取完消息,如果服务端有要返回给客户的消息那么就可以通过服务端的输出流发送给客户端,如果没有,直接关闭Socket。

        这种方式通过关闭客户端的输出流,告知服务端已经写完了,虽然可以读到服务端发送的消息,但是还是有一点点缺点:

    • 不能再次发送消息给服务端,如果再次发送,需要重新建立Socket连接

      这个缺点,在访问频率比较高的情况下将是一个需要优化的地方。

    3.通过约定符号

      这种方式的用法,就是双方约定一个字符或者一个短语,来当做消息发送完成的标识,通常这么做就需要改造读取方法。

      假如约定单端的一行为end,代表发送完成,例如下面的消息,end则代表消息发送完成


    static final String end="bye";

    Socket socket = server.accept(); // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取 BufferedReader read=new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8")); String line; StringBuilder sb = new StringBuilder(); while ((line = read.readLine()) != null && "end".equals(line)) { //注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8 sb.append(line); }

     可以看见,服务端不仅判断是否读到了流的末尾,还判断了是否读到了约定的末尾。

      这么做的优缺点如下:

    • 优点:不需要关闭流,当发送完一条命令(消息)后可以再次发送新的命令(消息)
    • 缺点:需要额外的约定结束标志,太简单的容易出现在要发送的消息中,误被结束,太复杂的不好处理,还占带宽

    4 .通过指定长度

    • 1个字节:最大256,表示256B
    • 2个字节:最大65536,表示64K
    • 3个字节:最大16777216,表示16M
    • 4个字节:最大4294967296,表示4G

    这个时候是不是很纠结,最大的当然是最保险的,但是真的有必要选择最大的吗,其实如果你稍微了解一点UTF-8的编码方式,那么你就应该能想到为什么一定要固定表示长度字节的长度呢,我们可以使用变长方式来表示长度的表示,比如:
    • 第一个字节首位为0:即0XXXXXXX,表示长度就一个字节,最大128,表示128B
    • 第一个字节首位为110,那么附带后面一个字节表示长度:即110XXXXX 10XXXXXX,最大2048,表示2K
    • 第一个字节首位为1110,那么附带后面二个字节表示长度:即110XXXXX 10XXXXXX 10XXXXXX,最大131072,表示128K
    • 依次类推

    如果用作命名发送,两个字节就够了,如果还不放心4个字节基本就能满足你的所有要求,下面的例子我们将采用2个字节表示长度,目的只是给你一种思路,让你知道有这种方式来获取消息的结尾:
    package yiwangzhibujian.waitreceive2; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; public class SocketServer { public static void main(String[] args) throws Exception { // 监听指定的端口 int port = 55533; ServerSocket server = new ServerSocket(port); // server将一直等待连接的到来 System.out.println("server将一直等待连接的到来"); Socket socket = server.accept(); // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取 InputStream inputStream = socket.getInputStream(); byte[] bytes; // 因为可以复用Socket且能判断长度,所以可以一个Socket用到底 while (true) { // 首先读取两个字节表示的长度 int first = inputStream.read(); //如果读取的值为-1 说明到了流的末尾,Socket已经被关闭了,此时将不能再去读取 if(first==-1){ break; } int second = inputStream.read(); int length = (first << 8) + second; // 然后构造一个指定长的byte数组 bytes = new byte[length]; // 然后读取指定长度的消息即可 inputStream.read(bytes); System.out.println("get message from client: " + new String(bytes, "UTF-8")); } inputStream.close(); socket.close(); server.close(); } }

     此处的读取步骤为,先读取两个字节的长度,然后读取消息,客户端为:
    package yiwangzhibujian.waitreceive2; import java.io.OutputStream; import java.net.Socket; public class SocketClient { public static void main(String args[]) throws Exception { // 要连接的服务端IP地址和端口 String host = "127.0.0.1"; int port = 55533; // 与服务端建立连接 Socket socket = new Socket(host, port); // 建立连接后获得输出流 OutputStream outputStream = socket.getOutputStream(); String message = "你好 yiwangzhibujian"; //首先需要计算得知消息的长度 byte[] sendBytes = message.getBytes("UTF-8"); //然后将消息的长度优先发送出去 outputStream.write(sendBytes.length >>8); outputStream.write(sendBytes.length); //然后将消息再次发送出去 outputStream.write(sendBytes); outputStream.flush(); //==========此处重复发送一次,实际项目中为多个命名,此处只为展示用法 message = "第二条消息"; sendBytes = message.getBytes("UTF-8"); outputStream.write(sendBytes.length >>8); outputStream.write(sendBytes.length); outputStream.write(sendBytes); outputStream.flush(); //==========此处重复发送一次,实际项目中为多个命名,此处只为展示用法 message = "the third message!"; sendBytes = message.getBytes("UTF-8"); outputStream.write(sendBytes.length >>8); outputStream.write(sendBytes.length); outputStream.write(sendBytes); outputStream.close(); socket.close(); } }

     客户端要多做的是,在发送消息之前先把消息的长度发送过去。

      这种事先约定好长度的做法解决了之前提到的种种问题,Redis的Java客户端Jedis就是用这种方式实现的这种方式的缺点:

    • 暂时还没发现

      当然如果是需要服务器返回结果,那么也依然使用这种方式,服务端也是先发送结果的长度,然后客户端进行读取。当然现在流行的就是,长度+类型+数据模式的传输方式。

     

    服务端并发处理能力

    在上面的例子中,服务端仅仅只是接受了一个Socket请求,并处理了它,然后就结束了,但是在实际开发中,一个Socket服务往往需要服务大量的Socket请求,那么就不能再服务完一个Socket的时候就关闭了,这时候可以采用循环接受请求并处理的逻辑:

    新手写法,有严重问题,不推荐:

    package yiwangzhibujian.multiserver;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class SocketServer {
      public static void main(String args[]) throws IOException {
        // 监听指定的端口
        int port = 55533;
        ServerSocket server = new ServerSocket(port);
        // server将一直等待连接的到来
        System.out.println("server将一直等待连接的到来");
        
        while(true){
          Socket socket = server.accept();
          // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
          InputStream inputStream = socket.getInputStream();
          byte[] bytes = new byte[1024];
          int len;
          StringBuilder sb = new StringBuilder();
          while ((len = inputStream.read(bytes)) != -1) {
            // 注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
            sb.append(new String(bytes, 0, len, "UTF-8"));
          }
          System.out.println("get message from client: " + sb);
          inputStream.close();
          socket.close();
        }
        
      }
    }


    服务端每次接收到客户端的请求后,都会创建一个新的线程去处理,而jvm的线程数量过多是,服务端处理速度会变慢,而且当一个请求的处理比较耗时的时候,后面的请求将被阻塞。

      我们可以用线程池解决。

          线程池的优点:

    • 线程复用,创建线程耗时,回收线程慢

    • 防止短时间内高并发,指定线程池大小,超过数量将等待,方式短时间创建大量线程导致资源耗尽,服务挂掉


    服务端代码如下:


    package
    com.example.admin.server;

    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;

    import java.net.Socket;

    public class thread extends Thread {

    private Socket socket;
    public thread(Socket socket){
    this.socket=socket;
    }


    @Override
    public void run() {
    try {
    System.out.println("当前线程:"+Thread.currentThread().getName());
    InputStream inputStream = socket.getInputStream();
    byte[] bytes;
    while (true) {
    int first = inputStream.read();
    if (first == -1) {
    break;
    }
    int second = inputStream.read();
    int length = (first >> 8) + second;
    bytes = new byte[length];
    inputStream.read(bytes);
    System.out.println(new String(bytes, "UTF-8"));
    }

    OutputStream outputStream = socket.getOutputStream();
    outputStream.write("Welcome to the new world!".getBytes("UTF-8"));
    inputStream.close();
    outputStream.close();
    } catch (IOException e) {
    System.out.print("null localHost");
    }
    }
    }



    package com.example.admin.server;


    import java.io.IOException;
    import java.net.InetAddress;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;



    public class MyClass {

    public static void main(String[] arugs) throws IOException {
            //线程过多,渣电脑小心卡死...
    ExecutorService service = Executors.newFixedThreadPool(3);
    ServerSocket serverSocket = new ServerSocket(5050);
    InetAddress address = InetAddress.getLocalHost();
    String ip = address.getHostAddress();
    System.out.println("服务端ip地址:" + ip);
    Socket socket = null;

    while (true) {
    socket = serverSocket.accept();
    service.execute(new thread(socket));
    }
    }

    }
    客户端代码同上

    多次点击发送
    控制台输出: 

    服务端ip地址:xxx.xxx.x.x
    当前线程:pool-1-thread-1
    one
    two
    当前线程:pool-1-thread-2
    one
    two
    当前线程:pool-1-thread-3
    one
    two
    当前线程:pool-1-thread-1
    one
    two

    服务端其他属性

    • SO_TIMEOUT:表示等待客户连接的超时时间。一般不设置,会持续等待。
    • SO_REUSEADDR:表示是否允许重用服务器所绑定的地址。一般不设置,经我的测试没必要,下面会进行详解。
    • SO_RCVBUF:表示接收数据的缓冲区的大小。一般不设置,用系统默认就可以了。

           体详细的解释可以参照下面。

    Socket的其它知识

      其实如果经常看有关网络编程的源码的话,就会发现Socket还是有很多设置的,可以学着用,但是还是要有一些基本的了解比较好。下面就对Socket的Java API中涉及到的进行简单讲解。首先呢Socket有哪些可以设置的选项,其实在SocketOptions接口中已经都列出来了:

    • int TCP_NODELAY = 0x0001:对此连接禁用 Nagle 算法。
    • int SO_BINDADDR = 0x000F:此选项为 TCP 或 UDP 套接字在 IP 地址头中设置服务类型或流量类字段。
    • int SO_REUSEADDR = 0x04:设置套接字的 SO_REUSEADDR。
    • int SO_BROADCAST = 0x0020:此选项启用和禁用发送广播消息的处理能力。
    • int IP_MULTICAST_IF = 0x10:设置用于发送多播包的传出接口。
    • int IP_MULTICAST_IF2 = 0x1f:设置用于发送多播包的传出接口。
    • int IP_MULTICAST_LOOP = 0x12:此选项启用或禁用多播数据报的本地回送。
    • int IP_TOS = 0x3:此选项为 TCP 或 UDP 套接字在 IP 地址头中设置服务类型或流量类字段。
    • int SO_LINGER = 0x0080:指定关闭时逗留的超时值。
    • int SO_TIMEOUT = 0x1006:设置阻塞 Socket 操作的超时值: ServerSocket.accept(); SocketInputStream.read(); DatagramSocket.receive(); 选项必须在进入阻塞操作前设置才能生效。
    • int SO_SNDBUF = 0x1001:设置传出网络 I/O 的平台所使用的基础缓冲区大小的提示。
    • int SO_RCVBUF = 0x1002:设置传入网络 I/O 的平台所使用基础缓冲区的大小的提示。
    • int SO_KEEPALIVE = 0x0008:为 TCP 套接字设置 keepalive 选项时
    • int SO_OOBINLINE = 0x1003:置 OOBINLINE 选项时,在套接字上接收的所有 TCP 紧急数据都将通过套接字输入流接收。

    客户端绑定端口

      服务端绑定端口是可以理解的,因为要监听指定的端口,但是客户端为什么要绑定端口,说实话我觉得这么做的人有点2,或许有的网络安全策略配置了端口访出,使用户只能使用指定的端口,那么这样的配置也是挺2的,直接说就可以不要留面子。

      当然首先要理解的是,如果没有指定端口的话,Socket会自动选取一个可以用的端口,不用瞎操心的。

      但是你非得指定一个端口也是可以的,做法如下,这时候就不能用Socket的构造方法了,要一步一步来:

    
    
    // 要连接的服务端IP地址和端口
    String host = "localhost"; 
    int port = 55533;
    // 与服务端建立连接
    Socket socket = new Socket();
    socket.bind(new InetSocketAddress(55534));
    socket.connect(new InetSocketAddress(host, port));
    这样做就可以了,但是当这个程序执行完成以后,再次执行就会报,端口占用异常:

    java.net.BindException: Address already in use: connect

    明明上一个Socket已经关闭了,为什么再次使用还会说已经被占用了呢?如果你是用netstat 命令来查看端口的使用情况:


    netstat -n|findstr "55533"
    TCP 127.0.0.1:55534 127.0.0.1:55533 TIME_WAIT
    简单来说,当连接主动关闭后,端口状态变为TIME_WAIT,其他程序依然不能使用这个端口,防止服务端因为超时重新发送的确认连接断开对新连接的程序造成影响。

      TIME_WAIT的时间一般有底层决定,一般是2分钟,还有1分钟和30秒的。

      所以,客户端不要绑定端口,不要绑定端口,不要绑定端口。

    读超时SO_TIMEOUT

      读超时这个属性还是比较重要的,当Socket优化到最后的时候,往往一个Socket连接会一直用下去,那么当一端因为异常导致连接没有关闭,另一方是不应该持续等下去的,所以应该设置一个读取的超时时间,当超过指定的时间后,还没有读到数据,就假定这个连接无用,然后抛异常,捕获异常后关闭连接就可以了,调用方法为:

    public void setSoTimeout(int timeout)
    throws SocketException

      timeout - 指定的以毫秒为单位的超时值。设置0为持续等待下去。建议根据网络环境和实际生产环境选择。

      这个选项设置的值将对以下操作有影响:

    • ServerSocket.accept()
    • SocketInputStream.read()
    • DatagramSocket.receive()

     

    判断Socket是否可用

      当需要判断一个Socket是否可用的时候,不能简简单单判断是否为null,是否关闭,下面给出一个比较全面的判断Socket是否可用的表达式,这是根据Socket自身的一些状态进行判断的,它的状态有:

    • bound:是否绑定
    • closed:是否关闭
    • connected:是否连接
    • shutIn:是否关闭输入流
    • shutOut:是否关闭输出流
    socket != null && socket.isBound() && !socket.isClosed() && socket.isConnected()&& !socket.isInputShutdown() && !socket.isOutputShutdown()


    建议如此使用,但这只是第一步,保证Socket自身的状态是可用的,
    但是当连接正常创建后,上面的属性如果不调用本方相应的方法是不会改变的,也就是说如果网络断开、服务器主动断开,Java底层是不会检测到连接断开并改变Socket的状态,
    所以,真实的检测连接状态还是得通过额外的手段,有两种方式。

    1 自定义心跳包

      双方需要约定,什么样的消息属于心跳包,什么样的消息属于正常消息,假设你看了上面的内容就容易理解了,我们定义前两个字节为消息的长度,那么我们就可以定义第3个字节为消息的属性,可以指定一位为消息的类型,1为心跳,0为正常消息。那么要做的有如下:

    • 客户端发送心跳包
    • 服务端获取消息判断是否是心跳包,若是丢弃
    • 当客户端发送心跳包失败时,就可以断定连接不可用

      具体的编码不再贴出,自己实现即可。

    2通过发送紧急数据

      Socket自带一种模式,那就是发送紧急数据,这有一个前提,那就是服务端的OOBINLINE不能设置为true,它的默认值是false。

      OOBINLINE的true和false影响了什么:

    • 对客户端没有影响
    • 对服务端,如果设置为true,那么服务端将会捕获紧急数据,这会对接收数据造成混淆,需要额外判断

      发送紧急数据通过调用Socket的方法:

        socket.sendUrgentData(0);

      发送数据任意即可,因为OOBINLINE为false的时候,服务端会丢弃掉紧急数据。

      当发送紧急数据报错以后,我们就会知道连接不通了。

    
    

    真得需要判断连接断开吗

      通过上面的两种方式已经可以判断出连接是否可用,然后我们就可以进行后续操作,可是请大家认真考虑下面的问题:

    1. 发送心跳成功时确认连接可用,当再次发送消息时能保证连接还可用吗?即便中间的间隔很短
    2. 如果连接不可用了,你会怎么做?重新建立连接再次发送数据?还是说单单只是记录日志?
    3. 如果你打算重新建立连接,那么发送心跳包的意义何在?为何不在发送异常时再新建连接?

      如果你认真考虑了上面的问题,那么你就会觉得发送心跳包完全是没有必要的操作,通过发送心跳包来判断连接是否可用是通过捕获异常来判断的。那么我们完全可以在发送消息报出IO异常的时候,在异常中重新发送一次即可,这两种方式的编码有什么不同呢,下面写一写伪代码。

    
    

      提前检测连接是否可用:

    //有一个连接中的socket
    Socket socket=...
    //要发送的数据
    String data="";
    try{
        //发送心跳包或者紧急数据,来检测连接的可用性
    }catch (Excetption e){
        //打印日志,并重连Socket
        socket=new Socket(host,port);
    }
    socket.write(data);
    直接发送数据,出异常后重新连接再次发送:
    //有一个连接中的socket
    Socket socket=...
    //要发送的数据
    String data="";
    try{
        socket.write(data);
    }catch (Excetption e){
        //打印日志,并重连Socket
        socket=new Socket(host,port);
        socket.write(data);
    }
    通过比较可以发现两种方式的特点,现在简单介绍下:
    • 两种方式均可实现连接断开重新连接并发送
    • 提前检测,再每次发送消息的时候都要检测,影响效率,占用带宽

      希望大家认真考虑,做出自己的选择。

     

    设置端口重用SO_REUSEADDR 

    
    

      首先,创建Socket时,默认是禁止的,设置true有什么作用呢,Java API中是这么介绍的:

    
    

    关闭 TCP 连接时,该连接可能在关闭后的一段时间内保持超时状态(通常称为 TIME_WAIT 状态或 2MSL 等待状态)。对于使用已知套接字地址或端口的应用程序而言,如果存在处于超时状态的连接(包括地址和端口),可能不能将套接字绑定到所需的 SocketAddress 上。

    
    

    使用 bind(SocketAddress) 绑定套接字前启用 SO_REUSEADDR 允许在上一个连接处于超时状态时绑定套接字。

    
    

      一般是用在绑定端口的时候使用,但是经过我的测试建议如下:

    
    
    • 服务端绑定端口后,关闭服务端,重新启动后不会提示端口占用
    • 客户端绑定端口后,关闭,即便设置ReuseAddress为true,即便能绑定端口,连接的时候还是会报端口占用异常
    
    

    设置关闭等待SO_LINGER

      Java API的介绍是:启用/禁用具有指定逗留时间(以秒为单位)的 SO_LINGER。最大超时值是特定于平台的。 该设置仅影响套接字关闭。 

      大家都是这么说的,当调用Socket的close方法后,没有发送的数据将不再发送,设置这个值的话,Socket会等待指定的时间发送完数据包。说实话,经过我简单的测试,对于一般数据量来说,几十K左右,即便直接关闭Socket的连接,服务端也是可以收到数据的。

      所以对于一般应用没必要设置这个值,当数据量发送过大抛出异常时,再来设置这个值也不晚。那么到达逗留超时值时,套接字将通过 TCP RST 强制性 关闭。启用超时值为零的选项将立即强制关闭。如果指定的超时值大于 65,535,则其将被减少到 65,535。

      综上所述,不建议绑定端口,也没必要设置ReuseAddress,当然ReuseAddress的底层还是和硬件有关系的,或许在你的机器上测试结果和我不一样,若是如此和平台相关性差异这么大配置更是不建议使用了。

     

    设置发送延迟策略TCP_NODELAY

    
    

      一般来说当客户端想服务器发送数据的时候,会根据当前数据量来决定是否发送,如果数据量过小,那么系统将会根据Nagle 算法(暂时还没研究),来决定发送包的合并,也就是说发送会有延迟,这在有时候是致命的,比如说对实时性要求很高的消息发送,在线对战游戏等,即便数据量很小也要求立即发送,如果稍有延迟就会感觉到卡顿,默认情况下Nagle 算法是开启的,所以如果不打算有延迟,最好关闭它。这样一旦有数据将会立即发送而不会写入缓冲区。

    
    

      但是对延迟要求不是特别高下还是可以使用的,还是可以提升网络传输效率的。

    设置输出输出缓冲区大小SO_RCVBUF/SO_SNDBUF

    • SO_SNDBUF:发送缓冲
    • SO_RCVBUF:接收缓冲

      默认都是8K,如果有需要可以修改,通过相应的set方法。不建议修改的太小,设置太小数据传输将过于频繁。太大了将会造成消息停留。

      不过我对这个经过测试后有以下结论:

    • 当数据填满缓冲区时,一定会发送
    • 当数据没有填满缓冲区时也会发送,这个算法还是上面说的Nagle 算法

    设置保持连接存活SO_KEEPALIVE

      虽然说当设置连接连接的读超时为0,即无限等待时,Socket不会被主动关闭,但是总会有莫名其妙的软件来检测你的连接是否有数据发送,长时间没有数据传输的连接会被它们关闭掉。

      因此通过设置这个选项为true,可以有如下效果:当2个小时(具体的实现而不同)内在任意方向上都没有跨越套接字交换数据,则 TCP 会自动发送一个保持存活的消息到对面。将会有以下三种响应:

    1. 返回期望的ACK。那么不通知应用程序(因为一切正常),2 小时的不活动时间过后,TCP 将发送另一个探头。
    2. 对面返回RST,表明对面挂了,但是又好了,Socket依然要关闭
    3. 没有响应,说明对面挂了,这时候关闭Socket

      所以对于构建长时间连接的Socket还是配置上SO_KEEPALIVE比较好。

    
    

    异常:java.net.SocketException: Connection reset by peer

    
    

      这个异常的含义是,我正在写数据的时候,你把连接给关闭了。这个异常在一般正常的编码是不会出现这个异常的,因为用户通常会判断是否读到流的末尾了,读到末尾才会进行关闭操作,如果出现这个异常,那就检查一下判断是否读到流的末尾逻辑是否正确。

    拆包和黏包

      使用Socket通信的时候,或多或少都听过拆包和黏包,如果没听过而去贸然编程那么偶尔就会碰到一些莫名其妙的问题,所有有这方面的知识还是比较重要的,至少知道怎么发生,怎么防范。

      现在先简单说明下拆包和黏包的原因:

    • 拆包:当一次发送(Socket)的数据量过大,而底层(TCP/IP)不支持一次发送那么大的数据量,则会发生拆包现象。
    • 黏包:当在短时间内发送(Socket)很多数据量小的包时,底层(TCP/IP)会根据一定的算法(指Nagle)把一些包合作为一个包发送。

      首先可以明确的是,大部分情况下我们是不希望发生拆包和黏包的(如果希望发生,什么都去做即可),那么怎么去避免呢,下面进行详解?

    黏包

      首先我们应该正确看待黏包,黏包实际上是对网络通信的一种优化,假如说上层只发送一个字节数据,而底层却发送了41个字节,其中20字节的I P首部、 20字节的T C P首部和1个字节的数据,而且发送完后还需要确认,这么做浪费了带宽,量大时还会造成网络拥堵。当然它还是有一定的缺点的,就是因为它会合并一些包会导致数据不能立即发送出去,会造成延迟,如果能接受(一般延迟为200ms),那么还是不建议关闭这种优化,如果因为黏包会造成业务上的错误,那么请改正你的服务端读取算法(协议),因为即便不发生黏包,在服务端缓存区也可能会合并起来一起提交给上层,推荐使用长度+类型+数据模式。

    如果不希望发生黏包,那么通过禁用TCP_NODELAY即可,Socket中也有相应的方法:

    void setTcpNoDelay(boolean on) 

      通过设置为true即可防止在发送的时候黏包,但是当发送的速率大于读取的速率时,在服务端也会发生黏包,即因服务端读取过慢,导致它一次可能读取多个包。

    拆包

      这个问题应该引起重视,在TCP/IP详解中说过:最大报文段长度(MSS)表示TCP传往另一端的最大块数据的长度。当一个连接建立时,连接的双方都要通告各自的 MSS。客户端会尽量满足服务端的要求且不能大于服务端的MSS值,当没有协商时,会使用值536字节。虽然看起来MSS值越大越好,但是考虑到一些其他情况,这个值还是不太好确定,具体详见《TCP/IP详解 卷1:协议》。

      如何应对拆包,其实在上面内容已经介绍过了,那就是如何表明发送完一条消息了,对于已知数据长度的模式,可以构造相同大小的数组,循环读取,示例代码如下:

    int length=1024;//这个是读取的到数据长度,现假定1024
    byte[] data=new byte[1024];
    int readLength=0;
    while(readLength<length){
        int read = inputStream.read(data, readLength, length-readLength);
        readLength+=read;
    }

    这样当循环结束后,就能读取到完整的一条数据,而不需要考虑拆包了。





    基于UDP协议的Socket的使用

    服务端
    
    
    package com.example.admin.server;

    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.net.SocketAddress;


    public class MyClass {

    public static void main(String[] args) throws IOException {
    DatagramSocket socket = null;
    try {
    socket = new DatagramSocket(8888);
    System.out.println("服务器开始监听消息");
    } catch (Exception e) {
    e.printStackTrace();
    }
    while (true) {
    byte data[] = new byte[1024];
    DatagramPacket packet = new DatagramPacket(data, data.length);
    socket.receive(packet);
    String result = new String(packet.getData(), packet.getOffset(), packet.getLength());
    System.out.println("客户端说: " + result);
    HandleThread handleThread = new HandleThread(socket,packet);
    handleThread.setPriority( 3 );
    handleThread.start();
    }
    }

    static class HandleThread extends Thread {

    private DatagramSocket mSocket;
    private DatagramPacket packet;

    public HandleThread(DatagramSocket mSocket ,DatagramPacket packet) {
    super();
    this.mSocket=mSocket;
    this.packet=packet;

    }

    @Override
    public void run() {
    try {
    byte[] sendData = "服务端说:Welcome to the new world!".getBytes("UTF-8");
    SocketAddress remoteAddress = packet.getSocketAddress();
    DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, remoteAddress);
    mSocket.send(sendPacket);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }
    }
     
    android客户端
    
    
    package com.example.admin.socket;


    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.TextView;
    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.net.InetAddress;

    public class MainActivity extends AppCompatActivity {

    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    textView=findViewById(R.id.textview);
    Button button=findViewById(R.id.button);

    button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    new Thread() {
    @Override
    public void run() {
    super.run();
    try {
    call();
    }catch (IOException e){
    Log.e("io","io close failed");
    }

    }
    }.start();}

    });
    }


    public void call()throws IOException {
    DatagramSocket mSocket=null;
    try {
    // 1.初始化DatagramSocket
    mSocket = new DatagramSocket();
    mSocket = new DatagramSocket();
    InetAddress address = InetAddress.getByName("xxx.xxx.x.x");//就是前面获取的服务器IP地址
    int port=8888;
    String sendData = "hello world";
    byte data[] = sendData.getBytes("UTF-8");
    DatagramPacket packet = new DatagramPacket(data, data.length, address, 8888);
    mSocket.send(packet);
    final byte[] receiveData = new byte[1024];
    DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
    mSocket.receive(receivePacket);
    runOnUiThread(new Runnable() {
    @Override
    public void run() {
    textView.setText(new String(receiveData));
    }
    });
    } catch (Exception e) {
    e.printStackTrace();
    System.out.println(e.getMessage());
    } finally {
    if (mSocket != null) {
    mSocket.close();
    }
    }
    }

    }
     
    控制台输出:服务器开始监听消息
    客户端说: hello world
    客户端说: hello world




    文章大量内容来自:https://www.cnblogs.com/yiwangzhibujian/p/7107785.html



     
  • 相关阅读:
    bzoj4974: [Lydsy1708月赛]字符串大师
    bzoj1801: [Ahoi2009]chess 中国象棋
    predis的使用
    常用的文件数据类型mime
    使用CURL模拟表单上传文件
    模型类:连接数据库
    extends注意事项
    extends前提
    include
    radio后的input框数据传递
  • 原文地址:https://www.cnblogs.com/neowu/p/10754075.html
Copyright © 2011-2022 走看看