TCP/IP基础上使用Java实现Socket通信

  • SeverSocket 服务端
  • Socket 客户端

DatagramSocket类处理UDP
SeverSocket、Socket、DatagramSocket均实现Closeable

基础知识

https://blog.csdn.net/Lzy410992/article/details/117123350

三层协议:

  • TCP传输控制协议: 可靠连接,连接建立有验证机制(三次握手),
  • UDP 用户数据报协议: 不建立连接,不可靠,没有连接管理能力,不负责重新发送丢失或出错的数据消息,没有流量控制能力。

TCP连接状态

image.png

图片地址

https://blog.csdn.net/Lzy410992/article/details/117123350
image.png

TCP长连接与短连接

  • 连接
    服务端与客户端彼此确认的过程,需要创建连接,TCP/IP中进行三次握手,成功后实现数据通信。
    创建好的1个连接中,TCP可以实现多次通信
  • 长连接
    可以实现服务端与客户端连接成功后连续传输数据,数据传输完毕后连接不关闭。
    TCP基于流的长连接,数据具有顺序行。建立Socket连接后无论是否使用都保持连接状态
    一次连接,多次通信,传输效率高
  • 短连接
    实现一次通信后,关闭连接。每次传输数据都要先创建连接
    一次连接,一次通信,传输效率低,http是典型的短链接

长连接优缺点

  • 优点
    除了建立连接3次握手外,不需要握手,直接传输数据,效率高
  • 缺点
    服务端保存多个Sokcet对象,大量占用服务器资源

短连接优缺点

  • 优点
    服务端不需要保存多个Sokcet对象,服务器资源占用低
  • 缺点
    每次都要重新创建连接,增加处理时间,传输效率低

UDP无状态传输

无连接的传输协议,无需建立连接就可以发送封装的 IP 数据包的方法。
UDP不存在长连接、短连接的概念

基于TCP的Socket通信

在Java体系中ServerSocket用于服务端,Socket用于客户端
代码层面即用Socket连接ServerSocket,即客户端主动连接服务端
image.png
image.png

Socket类、ServerSocket建立TCP连接

服务端对象,用户接收客户端Socket的连接

建立一个Socket连接 accept阻塞

服务端:需要在服务端创建一个ServerSocket实例,侦听该Socket实例。

  • 构造方法创建ServerSocket实例
    ServerSocket有4种public构造方法
    创建实例可以指定监听端口,传入队列最大长度,绑定的IP
    image.png
  • 调用accept方法监听该Socket
    返回一个Socket类实例
    该方法阻塞,直到建立连接

客户端:再在客户端创建Socket实例连接即可

  • 调用Socket构造方法创建实例
    Socket有8种public构造方法
    可以指定连接的IP、端口、代理、域名、(和是否为UDP连接,已经过时)
    image.png
//Socket通信
class AA{
    //服务端
    private static void server() {
        try ( ServerSocket serverSocket = new ServerSocket(8080);){
            System.out.println("创建8080端口Socket");
            serverSocket.accept();
            System.out.println("得到client通信");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //客户端
    private static void client()  {
        System.out.println("client创建连接");
       try( Socket client = new Socket("localhost", 8080);){
           System.out.println("client已经发送连接");
       } catch (IOException e) {
           e.printStackTrace();
       }
    }
    public static void main(String[] args) throws IOException, InterruptedException {
        new Thread(AA::server).start();
        System.out.println("main开始休眠");
        Thread.sleep(10000);
        System.out.println("main结束休眠");
        new Thread(AA::client).start();
    }
}

结果如下,accept是阻塞的
image.png

Socket read 阻塞

ServerSocket通过accept创建一个Socket,调用read读取一个InputStream在SocketInputStream实现类是阻塞的
image.png

//客户端read
class AB {
    //服务端
    private static void server() {
        InputStream inputStream=null;
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            System.out.println("创建8080端口Socket");
            //阻塞
            Socket accept = serverSocket.accept();
            System.out.println("得到client通信");
            inputStream = accept.getInputStream();
            //阻塞
            System.out.println("向client发送数据");
            inputStream.read("1234".getBytes(StandardCharsets.UTF_8));
            System.out.println("发送完毕");
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    //客户端
    private static void client() {
        System.out.println("client创建连接");
        try (Socket client = new Socket("localhost", 8080);) {
            System.out.println("client已经发送连接");
//            Thread.sleep(1000000);
        } catch (IOException
//                | InterruptedException
                e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        new Thread(AB::server).start();
        System.out.println("main开始休眠");
        Thread.sleep(1000);
        System.out.println("main结束休眠");
        new Thread(AB::client).start();
    }
}

//如果去掉注释,那么将会一直阻塞在read
image.png

client传递数据给server

  • 客户端,发送数据方
    调用socket获得outputstream,调用write方法
  • 服务端,接收数据方
    调用socket获得inputsream,调用read方法
//client传递数据给server
class AC {
    //服务端
    private static void server() {
        InputStream inputStream = null;
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            byte[] bytes = new byte[1024];
            Socket accept = serverSocket.accept();
            inputStream = accept.getInputStream();
            System.out.println("erver已经收到client,建立通信");
            while (inputStream.read(bytes)!=-1) {
                System.out.print(new String(bytes));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //客户端
    private static void client() {
        OutputStream outputStream = null;
        try (Socket client = new Socket("localhost", 8080);) {
            outputStream = client.getOutputStream();
            outputStream.write("我很好,你好吗?".getBytes());
            System.out.println("client已经发送连接");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        new Thread(AC::server).start();
        new Thread(AC::client).start();
    }
}

image.png

server传递数据给client

//server传递数据给client
class AD {
    //服务端
    private static void server() {
        OutputStream outputStream = null;
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            Socket accept = serverSocket.accept();
            outputStream = accept.getOutputStream();
            outputStream.write("这是server发送的信息!".getBytes());
            System.out.println("server已经发送连接");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    //客户端
    private static void client() {
        InputStream inputStream = null;
        try (Socket client = new Socket("localhost", 8080)) {
            inputStream = client.getInputStream();
            byte[] bytes = new byte[1024];
            while (inputStream.read(bytes) != -1) {
                System.out.print(new String(bytes));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        new Thread(AD::server).start();
        new Thread(AD::client).start();
    }
}

结果如下
image.png

传输多条数据

Outputstream可以write多次数据

//write多次数据
class AE{
    //服务端
    private static void server() {
        OutputStream outputStream = null;
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            Socket accept = serverSocket.accept();
            outputStream = accept.getOutputStream();
            outputStream.write("这是server发送的第1条信息!".getBytes());
            outputStream.write("这是server发送的第2条信息!".getBytes());
            outputStream.write("这是server发送的第3条信息!".getBytes());
            outputStream.write("这是server发送的第4条信息!".getBytes());
            outputStream.write("这是server发送的第5条信息!".getBytes());
            outputStream.write("这是server发送的第6条信息!".getBytes());
            System.out.println("server已经发送连接");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    //客户端
    private static void client() {
        InputStream inputStream = null;
        try (Socket client = new Socket("localhost", 8080)) {
            inputStream = client.getInputStream();
            byte[] bytes = new byte[1024];
            while (inputStream.read(bytes) != -1) {
                System.out.println(new String(bytes));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        new Thread(AE::server).start();
        new Thread(AE::client).start();
    }
}

只有2行,说明server虽然write了,但是打包发送了2次,不同情况下结果不同
image.png

client、server多次往来通信,长链接

实现多次长连接通信

//实现多次往来长连接
class AF {
    //服务端
    private static void server() {
        OutputStream outputStream = null;
        InputStream inputStream = null;
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            Socket accept = serverSocket.accept();
            outputStream = accept.getOutputStream();
            inputStream = accept.getInputStream();
            byte[] bytes = new byte[1024];

            outputStream.write("1.server发送第一条信息!".getBytes());
            outputStream.flush();

            inputStream.read(bytes);
            System.out.println(new String(bytes));

            outputStream.write("3.server发送第二条信息!".getBytes());
            outputStream.flush();

            inputStream.read(bytes);
            System.out.println(new String(bytes));

            outputStream.write("5.server发送第三条信息!".getBytes());
            outputStream.flush();
            
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                inputStream.close();
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    //客户端
    private static void client() {
        OutputStream outputStream = null;
        InputStream inputStream = null;
        try (Socket client = new Socket("localhost", 8080)) {
            inputStream = client.getInputStream();
            outputStream=client.getOutputStream();
            byte[] bytes = new byte[1024];

            inputStream.read(bytes);
            System.out.println(new String(bytes));

            outputStream.write("2.client发送第一条信息!".getBytes());
            outputStream.flush();

            inputStream.read(bytes);
            System.out.println(new String(bytes));

            outputStream.write("4.client发送第二条信息!".getBytes());
            outputStream.flush();

            inputStream.read(bytes);
            System.out.println(new String(bytes));

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                outputStream.close();
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        new Thread(AF::server).start();
        new Thread(AF::client).start();
    }
}

image.png

SocketOutputStream、SocketInputStream的close方法

调用该2类close时不但会关闭流,还会关闭socket
image.png
image.png

Socket传递大文件

client向server传递大文件byte数据

//传递大文件
//write多次数据
class AG {
    //服务端
    private static void server() {
        OutputStream outputStream = null;
        byte[] bytes = new byte[1024];
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            Socket accept = serverSocket.accept();
            outputStream = accept.getOutputStream();
            FileInputStream fileInputStream = new FileInputStream(Paths.get("/Users/xxxx/Desktop/GBT.pdf").toFile());
            while (fileInputStream.read(bytes)!=-1){
                outputStream.write(bytes);
            }
            fileInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    //客户端
    private static void client() {
        InputStream inputStream = null;
        try (Socket client = new Socket("localhost", 8080)) {
            inputStream = client.getInputStream();
            byte[] bytes = new byte[1024];
            FileChannel channel = new FileOutputStream(Paths.get("./test.PDF").toString()).getChannel();
            while (inputStream.read(bytes) != -1) {
                channel.write(ByteBuffer.wrap(bytes));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        new Thread(AG::server).start();
        new Thread(AG::client).start();
    }
}

结果如下图,文件可以正常打开浏览
image.png

多线程异步实现Socket

Socket结合Thread,client每发起一次新的请求就交给新的线程来执行业务

  • 非线程池实现
    因为accept是阻塞的,所以可以用死循环,不用担心负载
//server多线程
class BA{
    private static void server()  {
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            while (true) {
                try (Socket accept = serverSocket.accept()) {
                    new Thread(() -> {
                        try {
                            InputStreamReader reader = new InputStreamReader(accept.getInputStream());
                            char[] bytes = new char[32];
                            while (reader.read(bytes) != -1) {
                                System.out.println(new String(bytes));
                            }
                            reader.close();
                            accept.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }).start();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Thread(BA::server).start();

    }
}
  • 线程池实现
//server线程池
class BB {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        ExecutorService executorService = Executors.newFixedThreadPool(1000);
        while (true) {
            try {
                Socket accept = serverSocket.accept();
                executorService.execute(() -> {
                    try {
                        InputStreamReader reader = new InputStreamReader(accept.getInputStream());
                        char[] bytes = new char[32];
                        while (reader.read(bytes) != -1) {
                            System.out.println(new String(bytes));
                        }
                        reader.close();
                        accept.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

互传JavaBean对象

//传递JavaBean对象
class BC {
    static class Pepole implements Serializable {
        public Pepole(String name, Integer age) {
            this.age = age;
            this.name = name;
        }
        private String name;
        private Integer age;
        @Override
        public String toString() {
            return "Pepole{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    //服务端
    private static void server() {
        OutputStream outputStream = null;
        try (ServerSocket serverSocket = new ServerSocket(8989)) {
            Socket accept = serverSocket.accept();
            outputStream = accept.getOutputStream();
            Pepole superMan = new Pepole("SuperMan", 28);
           new ObjectOutputStream(outputStream).writeObject(superMan);
            System.out.println("服务端生成:" + superMan);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    //客户端
    private static void client() {
        InputStream inputStream = null;
        try (Socket client = new Socket("localhost", 8989)) {
            inputStream = client.getInputStream();
            try {
                Pepole pepole = (Pepole) new ObjectInputStream(inputStream).readObject();
                System.out.println("客户端读取:" + pepole);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(BC::server).start();
        new Thread(BC::client).start();
    }
}

image.png

抓包及Socket通信

抓包工具

WireShark和npcap的相关说明见下面博客
https://blog.csdn.net/HarveyH/article/details/113731485
wireshark在WIN系统下抓本地回路包有问题需要安装其他插件,可以使用局域网本机IP进行测试

TCP3次握手

  • 背景:2同一个局域网,一个模拟客户端,一个模拟服务端
    server在193.168.43.26
    client在192.168.43.147
class AE {
    //服务端
    private static void server() {
        OutputStream outputStream = null;
        try (ServerSocket serverSocket = new ServerSocket(8989)) {
            Socket accept = serverSocket.accept();
            outputStream = accept.getOutputStream();
            outputStream.write("这是server发送的第1条信息!".getBytes());
            outputStream.write("这是server发送的第2条信息!".getBytes());
            outputStream.write("这是server发送的第3条信息!".getBytes());
            outputStream.write("这是server发送的第4条信息!".getBytes());
            outputStream.write("这是server发送的第5条信息!".getBytes());
            outputStream.write("这是server发送的第6条信息!".getBytes());
            System.out.println("server已经发送连接");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    //客户端
    private static void client() {
        InputStream inputStream = null;
        try (Socket client = new Socket("192.168.43.26", 8989)) {
            inputStream = client.getInputStream();
            byte[] bytes = new byte[1024];
            while (inputStream.read(bytes) != -1) {
                System.out.println(new String(bytes));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

整个过程服务端抓包如下
image.png
具体的握手与挥手网上讲解
https://www.cnblogs.com/bj-mr-li/p/11106390.html
image.png

  • 第一次握手
    建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
    image.png
    176client发送SYN标志位到26server,代表要进行连接

  • 第二次握手
    发送SYN、ACK标志,ACK表示收到数据包确认,Ack=1表示server端想要client下一次发送的数据流序列号为1,Seq=0代表没有给client发送过数据

  • 第三次握手
    client反馈,发送Seq=1对应了server的期望。Ack=1,代表client想要server下次发送数据流序列号为1,Len=0代表没有数据传输

  • 总结三次握手大概意思
    1.client发送给server表示我想连接,请回复
    2.server向client发送反馈已经收到,请回复
    3,client向server反馈已收到答复

标志位SYN与ACK等

  • SYN、ACK
    具有自增特性
  • Seq
    TCP数据包中的序列号(Sequence Number)
    以传输的所有数据当作一个字节咧,Seq为整个字节六中每个字节的编号,不是以报文数量变好的
  • ACK
    报文到达确认,对接收到的数据的最高序列号的确认,并发送下次接受的希望的TCP数据包的序列号
    如下图,Seq=1,Len=37,则会继续发送想要接收的序列号即Seq=37+1=38
    image.png
  • PSH
    当调用outputStream.write("这是server发送的第1条信息!".getBytes());时,会向对方发送信息PSH和ACK表示希望立即处理信息
    之后client收到之后返回Ack=38表示之前的都已经收到了,期再server再次38的字节。

TCP4次挥手

我的代码client端也是有发送数据给server接收的,上面的代码不是我在client端运行的代码(懒得重新弄了)
image.png
上图的RST表示server需要重新请求复位连接,同时也是server发起挥手请求的

  • 第一次挥手
    client发起关闭请求FIN、ACK标志位,结束绘画
  • 第二次挥手
    server回应ACK
  • 第三次挥手
    Server回应FIN、ACK
  • 第四次挥手
    client回应ACK

3次握手在Java代码中的时机

在ServerSocket和Socket对象被创建出来时就会进行连接,而不是等到调用accept方法

ServerSocket类

image.png

accept与setSoTimeout方法

  • accept 监听连接,阻塞
public Socket accept() throws IOException
  • setSoTimeout 设置超时时间
public synchronized void setSoTimeout(int timeout) throws SocketException
  • getSoTimeout 获得超时时间
public synchronized int getSoTimeout() throws IOException
  • 注意超时时间只是针对Socket对象,ServerSocket对象还是有效的

ServerSocket构造方法

public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
  • port 端口号,为0则自动分配端口,在0~65535之间分配空闲端口
  • backlog 最大等待队列长度,超过该长度则拒绝,小于1,默认为50
  • InetAddress 代表可以在ServerSocket的多宿主机上使用,一台计算机可能会有多个网卡,对应多个不同的IP,或者一个网卡有多个IP

bind绑定IP和Port

public void bind(SocketAddress endpoint, int backlog) throws IOException

使用的场景为调用ServerSocket的无参构造方法之后想指定IP和Port、最大等待队列长度
SocketAddress为抽象类,其实现类为InetSocketAddress
image.png

获取ServerSocket绑定的Port及SocketAddress

  • 获得Port
public int getLocalPort() 
  • 获得SocketAddress,即可以获得Port、IP、HostName
public SocketAddress getLocalSocketAddress()

关闭与获取ServerSocket状态

关闭ServerSocket,则所有还在阻塞的accept方法会抛出异常

public void close() throws IOException
public boolean isClosed()

端口复用

启用/禁用SO_REUSEADDR套接字选项
当 TCP 连接关闭时,连接可能会在连接关闭后的一段时间内保持超时状态(通常称为TIME_WAIT状态或2MSL等待状态)。 对于使用众所周知的套接字地址或端口的应用程序,如果存在涉及套接字地址或端口的处于超时状态的连接,则可能无法将套接字绑定到所需的SocketAddress 。

public void setReuseAddress(boolean on) throws SocketException

TIME_WAIT状态

server与client建立TCP连接之后,主动关闭的一方会进入TIME_WAIT状态,再停留若干时间进入CLOSED状态,在linux中应用程序可以复用处于TIME_WAIT状态的端口

设置ServerSocket缓冲区窗口大小

果应用程序希望允许大于 64K 字节的接收窗口,如 RFC1323 所定义,那么必须在 ServerSocket 绑定到本地地址之前设置建议值。 这意味着,必须使用无参数构造函数创建 ServerSocket,然后必须调用 setReceiveBufferSize(),最后通过调用 bind() 将 ServerSocket 绑定到地址。

public synchronized void setReceiveBufferSize (int size) throws SocketException

Socket类的使用

ServerSocket用于搭建server端环境,Socket用于server和client端通信

client的Socket绑定指定Port端口

Socket如果没有传入端口是默认空闲端口的,也可以指定端口
用bind绑定指定IP、Port端口

 public void bind(SocketAddress bindpoint)

后再用connetc方法执行连接到指定IP端口

public void connect(SocketAddress endpoint) throws IOException
//client绑定指定IP、Port
class DA{
    private static void server(){
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            Socket accept = serverSocket.accept();
            accept.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private static void client(){
        Socket socket = new Socket();
        try {
            socket.bind(new InetSocketAddress(8081));
            socket.connect(new InetSocketAddress(8080));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
     new Thread(DA::server).start();
     new Thread(DA::client).start();
    }
}

connect的超时设置

将此套接字连接到具有指定超时值的服务器。 零超时被解释为无限超时。 然后连接将阻塞,直到建立或发生错误。如果为0则无超时时间。WIN默认为20S

public void connect(SocketAddress endpoint, int timeout) throws IOException 

获得远程端口及本地端口

获得远程端口

public int getPort()

获得本地端口

public int getLocalPort()
//client绑定指定IP、Port,获得端口
class DB{
    private static void server(){
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            Socket accept = serverSocket.accept();
            accept.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private static void client(){
        Socket socket = new Socket();
        try {
            socket.bind(new InetSocketAddress(8081));
            socket.connect(new InetSocketAddress(8080));
            System.out.println(socket.getPort());
            System.out.println(socket.getLocalPort());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        new Thread(DB::server).start();
        new Thread(DB::client).start();
    }
}

结果如下,远程server为8080,本地client为8081
image.png

获得本地及远程Address

获得本地Socket Address信息

	public InetAddress getLocalAddress()

获得Socket绑定的远程Address信息

	public SocketAddress getLocalSocketAddress()
//获得本地及远程Address
class DC{
    private static void server(){
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            Socket accept = serverSocket.accept();
            InetAddress localAddress = accept.getLocalAddress();
            byte[] address = localAddress.getAddress();
            for (int i = 0; i < address.length; i++) {
                System.out.print(address[i]);
            }
            System.out.println();
            InetSocketAddress inetSocketAddress = (InetSocketAddress) accept.getLocalSocketAddress();
            System.out.println("server端收到的client端为:"+inetSocketAddress.getAddress()+" "+inetSocketAddress.getPort());
            accept.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private static void client(){
        Socket socket = new Socket();
        try {
            socket.bind(new InetSocketAddress(8081));
            socket.connect(new InetSocketAddress(8080));
            System.out.println(socket.getPort());
            System.out.println(socket.getLocalPort());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Thread(DC::server).start();
        new Thread(DC::client).start();
    }
}

image.png

Socket状态判断

  • Socket的绑定状态
    返回套接字的绑定状态,关闭套接字不会清除其绑定状态,这意味着如果在关闭之前成功绑定,则此方法将为关闭的套接字返回true
public boolean isBound()
  • Socket的连接状态
public boolean isConnected()
  • Socket的关闭状态
public boolean isClosed()
//Socket状态判断:绑定、关闭、连接
class DD {
    private static void server() {
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            Socket accept = serverSocket.accept();
            accept.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private static void client() {
        Socket socket = new Socket();
        System.out.println(socket.isClosed());
        try {
            System.out.println(socket.isBound());
            socket.bind(new InetSocketAddress(8081));
            System.out.println(socket.isBound());
            System.out.println(socket.isConnected());
            socket.connect(new InetSocketAddress(8080));
            System.out.println(socket.isConnected());
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.out.println(socket.isClosed());
    }
    public static void main(String[] args) {
        new Thread(DD::server).start();
        new Thread(DD::client).start();
    }
}

结果如下图
image.png

半读半写

  • 半读
    即Socket的一方禁用输入流,无法读取
 public void shutdownInput() throws IOException
  • 半写
    即Socket的一方禁用输出流,无法写入
public void shutdownOutput() throws IOException
//半读半写
class DE{
    private static void server() {

        try (ServerSocket serverSocket = new ServerSocket(8080);) {
            Socket accept = serverSocket.accept();
            InputStream inputStream = accept.getInputStream();
            System.out.println("server Input:"+inputStream.available());
            byte[] bytes = new byte[1024];
            while (inputStream.read(bytes)!=-1){
                System.out.println(new String(bytes, StandardCharsets.UTF_8));
            }
            accept.shutdownInput();
            System.out.println("server Input:"+inputStream.available());
            InputStream inputStream1 = accept.getInputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private static void client() {
        Socket socket = new Socket();
        try {
            socket.bind(new InetSocketAddress(8081));
            socket.connect(new InetSocketAddress(8080));
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write("你好".getBytes(StandardCharsets.UTF_8));
            socket.shutdownOutput();
            outputStream.write("你还好?".getBytes(StandardCharsets.UTF_8));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        new Thread(DE::server).start();
        new Thread(DE::client).start();
    }
}

结果如下
image.png

半读半写状态判断

  • 判断读是否关闭
public boolean isInputShutdown()

判断写是否关闭

public boolean isOutputShutdown()

禁用/启用 Nagle 算法

image.png
Java默认为false,即有延迟、启用Nagle算法

  • 设置 true为on,false为off
public void setTcpNoDelay(boolean on) throws SocketException
  • 查询是否启用
public boolean getTcpNoDelay() throws SocketException

Nagle 算法

Nagle算法的百度百科释义
https://baike.baidu.com/item/Nagle%E7%AE%97%E6%B3%95/5645172?fr=aladdin
简而言之:为了解决小数据包过多,造成网络拥堵,采用DELAY延迟策略,将多个数据包合并发送没这样TCP/IP协议头所占比重小

  • 场景
    1.不适用于:
    Nagle算法是有延迟性的,所以如果要求高实时性,例如游戏和其他高交互性场景则不适用。那么设置为true,即NODELAY无延迟,放弃Nagle算法
    2.适用于:
    大包、高延迟场合

设置、查询接受和发送缓冲区大小

public synchronized void setReceiveBufferSize(int size)
    throws SocketException
public synchronized int getReceiveBufferSize()
    throws SocketException
public synchronized void setSendBufferSize(int size)
    throws SocketException
public synchronized int getSendBufferSize() throws SocketException

探测连接是否失效

在创建client和server时,设置为true,若对方在某个时间点没有发送数据过来,会探测双方的TCP/IP连接是否有效。如果不设置此选项,则无法知道对方是否宕机,会仍然保存失效的连接,如果设置了就会关闭连接
该方法不常用,叫常用的是使用轮询嗅探的方式判断是否为正常的连接

public void setKeepAlive(boolean on) throws SocketException

其他API

  • 设置关闭延迟时间linger
    在默认情况下执行close后该方法会立即返回,但底层的Socket不会立即关闭,会延迟一段时间将发送缓冲区的声誉数据在延迟时间内继续发送给对方后再关闭
public void setSoLinger(boolean on, int linger) throws SocketException

该方法是启用或禁用最大逗留延迟时间linger

  • 设置Socket的InputStream的read方法最大超时时间
    如果调用了read时间超过timeOut则引发异常,此时Socket仍可以正常使用继续获得InputStream。
    如果为0则被设置为无穷大等待时间
public synchronized void setSoTimeout(int timeout) throws SocketException
  • 发送与接收紧急数据OOBnline
    默认禁用会丢弃紧急数据,OOB为需要紧急发送的数据
public void setOOBInline(boolean on) throws SocketException

sendUrgentData方法用于发送进紧急数据,发送1个单byte的数据,立即发送出去

public void sendUrgentData (int data) throws IOException
  • 定性描述该Socket
    为从此 Socket 发送的数据包设置 IP 标头中的流量类别或服务类型八位字节。 由于底层网络实现可能会忽略此值,应用程序应将其视为一个提示。
public void setTrafficClass(int tc) throws SocketException

image.png

基于UDP的Socket通信

UDP(User Datagram Protocol)面向无连接的传输层协议,提供不可靠信息传输服务
通信时无需建立连接,直接把数据包从一端发送到另一端

  • 特点
    不可靠、没有顺序保证、没有流量控制、延迟小、数据传输效率高
    1.在网络环境不好的情况下,会丢失数据包,没有数据包重传功能
    2.发送报文后无法得知其是否安全,是否完整到达目的地
  • 应用场景
    在视频、音频等丢失数据包对结果影响不大的场景使用
    在Java体系中使用DatagramSocket对象来实现UDP通信

DatagramSocket实现UDP通信

image.png
DatagramSocket对象用于创建UDP连接
DatagramPacket对象用于发送的报文数据信息

//UDP简单连接
class EA {
    private static void server() {
        try {
            DatagramSocket datagramSocket = new DatagramSocket(8080);
            byte[] bytes = new byte[10];
            DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length);
            datagramSocket.receive(datagramPacket);
            datagramSocket.close();
            System.out.println("接收到的UDP为:" + new String(bytes, StandardCharsets.UTF_8));
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private static void client() {
        try {
            DatagramSocket datagramSocket = new DatagramSocket();
            datagramSocket.connect(new InetSocketAddress(8080));
            DatagramPacket datagramPacket = new DatagramPacket("123456".getBytes(), "123456".getBytes().length);
            datagramSocket.send(datagramPacket);
            datagramSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        new Thread(EA::server).start();
        new Thread(EA::client).start();
    }
}


这个家伙很懒,啥也没有留下😋