Bootstrap

使用Socket技术进行数据传输、传输文件;浏览器访问Socket服务器

计算机网络基础

利用通信线路和通信设备,将地理位置不同的、功能独立的多台计算机互连起来,以功能完善的网络软件来实现资源共享和信息传递,就构成了计算机网络系统

IP地址和端口

通过IP地址,区分不同的计算机

每一台电脑在一个网络上拥有一个独属于自己的IP地址,用于区别其他电脑,可以通过对方的IP地址进行访问;所以当我们的手机开着流量的时候,是无法访问连着WIFI的电脑的

电脑上运行的大量程序可能需要通过网络来访问其他计算机,这时通过端口号来进行区分

image-20230513220614495

因此一般看到的是地址都是IP:port形式访问目标主机的一个应用程序,端口号的值为[0, 65535]

IP地址分为IPv4和IPv6,IPv4类似于192.168.0.11,上面提到的例子都是使用的IPv4,它一共有四组数字,每组数字占8个bit位,IPv4地址0.0.0.0表示为2进制就是:00000000.00000000.00000000.00000000,共32个bit,最大为255.255.255.255

IPv6能够保存128个bit位,因此它也可以表示更多的IP地址,一个IPv6地址看起来像这样:1030::C9B4:FF12:48AA:1A2B,目前也正在向IPv6的阶段过渡

TCP和 UDP

  • TCP:当一台计算机想要与另一台计算机通讯时,两台计算机之间的通信需要畅通且可靠(会进行三次握手,断开也会进行四次挥手),这样才能保证正确收发数据,因此TCP更适合一些可靠的数据传输场景
  • UDP:它是一种无连接协议,数据不会建立可靠传输,传输过程中有可能会导致部分数据丢失,但是它比TCP传输更加简单高效,适合视频直播之类的

image-20230513220851141

Socket技术

通过Socket技术(它是计算机之间进行通信一种约定或一种方式),可以实现两台计算机之间的通信,Socket也被翻译为套接字,是操作系统底层提供的一项通信技术,它支持TCP和UDP。而Java就对socket底层支持进行了一套完整的封装,因此可以通过Java来实现Socket通信。

要实现Socket通信,需创建一个数据发送者和一个数据接收者,也就是客户端和服务端,我们需要提前启动服务端,来等待客户端的连接,而客户端只需要随时启动去连接服务端即可

//服务端
public static void main(String[] args) {
    try(ServerSocket server = new ServerSocket(8080)){    //将服务端创建在端口8080上
        System.out.println("正在等待客户端连接...");
        Socket socket = server.accept();  //当没有客户端连接时,线程会阻塞,直到有客户端连接为止
        System.out.println("客户端已连接,IP地址为:"+socket.getInetAddress().getHostAddress());
    }catch (IOException e){
        e.printStackTrace();
    }
}

用户端

public static void main(String[] args) {
    try (Socket socket = new Socket("localhost", 8080)){
        System.out.println("已连接到服务端!");
    }catch (IOException e){
        System.out.println("服务端连接失败!");
        e.printStackTrace();
    }
}

实际上他是一个TCP连接的过程,一旦连接建立,只要客户端不主动关闭,整个程序就不会停止运行;服务端也不仅仅支持一个客户端,在同一网络的前提下,可以连接多个服务端

public static void main(String[] args) {
	try(ServerSocket server = new ServerSocket(8080)) { //port,也就是端口号为8080
        System.out.println("等待连接...");
        while(true) {
            Socket s = server.accept(); //无限循环等待客户端的连接
            System.out.println("IP地址为:" + socket.getInetAddress().getHostAddress());
        }
    }catch(IOException e) {
        e.printStackTrace();
    }
}

使用Socket进行数据传输

通过Socket对象,我们就可以获取到对应的I/O流进行网络数据传输:

服务端

//Server端
public class server {
    public static void main(String[] args) {
        try(ServerSocket server = new ServerSocket(8080)){ //try-with-resource语句,自动关闭,不需要close
            System.out.println("正在等待客户端连接:");
            Socket socket = server.accept();
            socket.setSoTimeout(5000); // timeout
            System.out.println("客户端已连接,IP地址为:" + socket.getInetAddress().getHostAddress());
            System.out.println("读取客户端数据:");
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            System.out.println("内容为:" + reader.readLine());

            OutputStreamWriter writer = new OutputStreamWriter(socket.getOutputStream());
            writer.write("收到\n");
            writer.flush();
            socket.close();
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

用户端

public class Main {
    public static void main(String[] args) {
        try(Socket s = new Socket("localhost", 8080);
            Scanner sc = new Scanner(System.in)){
            System.out.println("已连接到服务区");
            OutputStreamWriter writer = new OutputStreamWriter(s.getOutputStream()); //读取发送内容
            writer.write(sc.nextLine() + "\n"); //加换行符是读取需要
            writer.flush();
            s.shutdownOutput();
            System.out.println("数据已发送");

            BufferedReader reader = new BufferedReader(new InputStreamReader(s.getInputStream()));
            System.out.println("收到服务端响应:" + reader.readLine());
        }catch(IOException e) {
            System.out.println("连接失败");
            e.printStackTrace();
        }finally {
            System.out.println("断开连接");
        }
    }
}

先启动Server端,再启动用户端;127.0.0.1是自己电脑

image-20230513204331818 image-20230513204351867

手动关闭单向的流:

socket.shutdownOutput(); //输出方向关闭
socket.shutdownInput(); //关闭输入方向

可以通过调用setSoTimeout()方法来设定IO超时时间:

socket.setSoTimeout(3000);

当超过设定时间都依然没有收到客户端或是服务端的数据时,会抛出异常:

java.net.SocketTimeoutException: Read timed out
	at java.net.SocketInputStream.socketRead0(Native Method)
	at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
	at java.net.SocketInputStream.read(SocketInputStream.java:171)
	at java.net.SocketInputStream.read(SocketInputStream.java:141)
	at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
	at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
	at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
	at java.io.InputStreamReader.read(InputStreamReader.java:184)
	at java.io.BufferedReader.fill(BufferedReader.java:161)
	at java.io.BufferedReader.readLine(BufferedReader.java:324)
	at java.io.BufferedReader.readLine(BufferedReader.java:389)
	at com.test.Main.main(Main.java:41)

也可以不自动链接

try (Socket socket = new Socket();
	Scanner sc = new Scanner(System.in)) {
		socket.connect(new InetSocketAddress("localhost", 8080), 1000);
	}

可能会出现连接双方出现意外却通知不到对方的情况,这个时候其中一方还持有联系,就会占用资源,可以使用setKeepAlive()的方法来防止:

socket.setKeepAlive(true);

当客户端连接之后,如果此函数设置值为true,当对方没有发送任何数据过来的时候,超过一个时间(要看系统内核参数配置),这边就会发送一个ack探测包,探测TCP/IP连接是否有效

100%弄明白5种IO模型 - 知乎

此缓冲区大小为:8192,我们可以手动调整其大小来优化传输效率:

socket.setReceiveBufferSize(25565);   //TCP接收缓冲区
socket.setSendBufferSize(25565);    //TCP发送缓冲区

使用Socket传输文件

实现文件传输

用户端

public class Main {
    public static void main(String[] args) {
        try(Socket s = new Socket("localhost", 8080)){
            FileInputStream fis = new FileInputStream("test.txt"); //输入文件
            OutputStream stream = s.getOutputStream(); //输出流
            byte[] bytes = new byte[1024];
            int len;
            while((len = fis.read(bytes)) != -1) {
                stream.write(bytes, 0, len);
            }
            fis.close();
            stream.flush();
            stream.close();
            System.out.println("文件已传输");
        }catch(IOException e) {
            System.out.println("连接失败");
            e.printStackTrace();
        }
    }
}

服务端

public class server {
    public static void main(String[] args) {
        try(ServerSocket server = new ServerSocket(8080)){
            Socket socket = server.accept();

           InputStream stream = socket.getInputStream();
           FileOutputStream file = new FileOutputStream("toserver.txt");
           byte[] buffer = new byte[1024];
            int len;
            while((len = stream.read(buffer)) != -1){
                file.write(buffer, 0, len);
            }
            file.flush();
            file.close();
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

使用浏览器访问Socket服务器

public class server {
    public static void main(String[] args) {
        try(ServerSocket server = new ServerSocket(8080)){
            System.out.println("正在等待客户端连接:");
            Socket socket = server.accept();
            System.out.println("IP地址为:" + socket.getInetAddress().getHostAddress());
            InputStream in = socket.getInputStream();
            while(true) {
                int read = in.read();
                if(read == -1) {
                    break;
                }
                System.out.print((char) read);
            }
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

启动Server端,用浏览器输入http://localhost:8080或是http://127.0.0.1:8080/访问;会发现浏览器无法访问,但是IDEA给了一堆反馈

image-20230513214310166

我们的服务端能够读取HTTP请求。但是Http协议并不会保持长连接,在得到我们响应的数据后会立即关闭TCP连接。

既然使用的是Http连接,如果我们的服务器要支持响应HTTP请求,那么就需要按照HTTP协议的规则,返回一个规范的响应文本,首先是响应头,它至少要包含一个响应码:

HTTP/1.1 200 Accpeted

然后就是响应内容(注意一定要换行再写),我们尝试来编写一下支持HTTP协议的响应内容:

public static void main(String[] args) {
    try(ServerSocket server = new ServerSocket(8080)){
        System.out.println("正在等待客户端连接...");
        Socket socket = server.accept();
        System.out.println("客户端已连接,IP地址为:"+socket.getInetAddress().getHostAddress());
        BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); 
        System.out.println("接收到客户端数据:");
        while (reader.ready()) System.out.println(reader.readLine()); 
        OutputStreamWriter writer = new OutputStreamWriter(socket.getOutputStream());
        writer.write("HTTP/1.1 200 Accepted\r\n");   //200是响应码,Http协议规定200为接受请求,400为错误的请求,404为找不到此资源(不止这些,还有很多)
        writer.write("\r\n");   //在请求头写完之后还要进行一次换行,然后写入响应实体(会在浏览器上展示的内容)
        writer.write("You're on your own Kids!");
        writer.flush();
    }catch (Exception e){
        e.printStackTrace();
    }
}

我们可以打开浏览器的开发者模式,我们来观察一下浏览器的实际请求过程。
image-20230513220047808

;