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

 

posted on 2020-05-31 16:12  鑫男  阅读(320)  评论(0编辑  收藏  举报

导航