java网络编程Socket实现客户端向服务端发送信息

(可按目录按需阅读,我一般会整理的比较细)

前置知识

java IO

Socket

什么是socket?socket字面意思其实就是一个插口或者套接字,包含了源ip地址、源端口、目的ip地址和源端口。
但是socket在那个位置呢 ,在TCP/IP网络的四层体系和OSI七层好像都找不到他的影子,如下图所示, Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。一般由操作系统或者JVM自己实现。java.net中的socket其实就是对底层的抽象调用。有一点需要注意,运行在同一主机上的其他应用程序可能也会通过底层套接字抽象来使用网络,因此会与java socket实例竞争资源,如端口。
java网络编程Socket实现客户端向服务端发送信息_第1张图片

工作流程
对于服务器来说,服务器先初始化socket,然后端口绑定(bind),再对端口监听(listen),调用accept阻塞,等待客户端连接请求。对于客户端来说,客户端初始化socket,然后申请连接(connection)。客户端申请连接,服务器接受申请并且回复申请许可(这里要涉及TCP三次握手连接),然后发送数据,最后关闭连接,这是一次交互过程。
java网络编程Socket实现客户端向服务端发送信息_第2张图片

ServerSocket

java.net.ServerSocket java 的实现
ServerSocket 和 Socket 不同,服务器套接字的角色是等待来自客户端的连接请求。一旦服务器套接字获得一个连接请求,它创建一个 Socket 实例来与客户端进行通信。

要创建一个服务器套接字,你需要使用 ServerSocket 类提供的四个构造方法中的一个。你 需要指定 IP 地址和服务器套接字将要进行监听的端口号。通常,IP 地址将会是 127.0.0.1,也 就是说,服务器套接字将会监听本地机器。服务器套接字正在监听的 IP 地址被称为是绑定地址。 服务器套接字的另一个重要的属性是 backlog,这是服务器套接字开始拒绝传入的请求之前,传 入的连接请求的最大队列长度。 其中一个 ServerSocket 类的构造方法如下所示:

public ServerSocket(int port, int backLog, InetAddress bindingAddress);

角色

服务器

服务器的socket程序有以下几个任务:

  • 创建ServerSocket。
  • 绑定并监听端口
  • 阻塞,等待客户端连接。
  • 与客户端连接成功后,进行数据交互

客户端

客户端的socket程序有以下几个任务:

  • 创建Socket。
  • 连接服务器。
  • 与服务器连接成功后,进行数据交互。

代码

使用Socket实现客户端和服务端的连接,其实网编程的本质就是进程之间的通信。服务端和客户端都会运行一个进程,然后进行数据交互,也就是数据的输入,输出。所以会涉及到IO

服务端代码

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    public static void main(String[] args) {

        final String QUIT = "quit";
        final int DEFAULT_PORT = 8000;
        ServerSocket serverSocket = null;
        BufferedReader reader = null;
        BufferedWriter writer = null;

        try {
            // 绑定监听端口
            serverSocket = new ServerSocket(DEFAULT_PORT);
            System.out.println("启动服务器,监听服务器本地端口" + DEFAULT_PORT);

            while (true) {
                // 等待客户端连接
                Socket socket = serverSocket.accept();
                System.out.println("客户端["+socket.getInetAddress()+":"+ socket.getPort() + "]已连接");

                reader = new BufferedReader(
                        new InputStreamReader(socket.getInputStream())
                );

                writer = new BufferedWriter(
                        new OutputStreamWriter(socket.getOutputStream())
                );

                String msg = null;
                while ((msg = reader.readLine()) != null) {
                    // 读取客户端发送的消息
                    System.out.println("客户端["+socket.getInetAddress()+":"+ socket.getPort() + "]: " + msg);

                    // 回复客户发送的消息
                    writer.write("服务器已收到: " + msg + "\n");
                    //防止消息遗留到本地缓冲区,保证马上发送出去
                    writer.flush();

                    // 查看客户端是否退出
                    if (QUIT.equalsIgnoreCase(msg)) {
                        System.out.println("客户端["+socket.getInetAddress()+":"+ socket.getPort() + "]已断开连接");
                        break;
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                serverSocket.close();
                reader.close();
                writer.close();
                System.out.println("关闭serverSocket");
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

    }
}

客户端代码

import java.io.*;
import java.net.Socket;

public class Client {


    public static void main(String[] args) {

        final String QUIT = "quit";
        final String DEFAULT_SERVER_HOST = "127.0.0.1";
        final int DEFAULT_SERVER_PORT = 8000;
        Socket socket = null;

        BufferedWriter writer = null;
        BufferedReader reader = null;
        BufferedReader consoleReader = null;

        try {
            // 创建socket
            socket = new Socket(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);

            // 创建IO流
            reader = new BufferedReader(
                    new InputStreamReader(socket.getInputStream())
            );
            writer = new BufferedWriter(
                    new OutputStreamWriter(socket.getOutputStream())
            );

            // 等待用户输入信息
            consoleReader = new BufferedReader(new InputStreamReader(System.in));

            while (true) {
                String input = consoleReader.readLine();

                // 发送消息给服务器
                writer.write(input + "\n");
                writer.flush();

                // 读取服务器返回的消息
                String msg = reader.readLine();
                System.out.println(msg);

                // 查看用户是否退出
                if (QUIT.equalsIgnoreCase(input)) {
                    break;
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {

            try {
                writer.close(); //关闭之前还会flush一次
                socket.close();
                reader.close();
                consoleReader.close();
                System.out.println("关闭socket");
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

    }
}

运行展示

客户端
java网络编程Socket实现客户端向服务端发送信息_第3张图片

服务端
java网络编程Socket实现客户端向服务端发送信息_第4张图片

CMD查看

开启Server端之后,在Windows cmd 终端里面输入命令,会发现8000端口处于LISTENING状态,先前没开启Server代码运行是没有的

netstat -ano|findstr "8000"

netstat 用于显示套接字内容 , -ano 是可选选项
a不仅显示正在通信的套接字,还显示包括尚未开始通信等状态的所有套接字
n 显示 IP 地址和端口号
o 显示套接字的程序 PID

java网络编程Socket实现客户端向服务端发送信息_第5张图片
第一列表示通信协议,这里是TCP
第二列表示,运行netstat命令的主机ip和port,这里也就是服务器的IP和port,0.0.0.0表示还没有绑定IP地址
第三列表示,通信对象的IP和port,0.0.0.0:0表示还没连接到对象,所以IP和port都不知道
第四列表示,LISTENING表示等待对方连接
最后一列,PID进程号
图中的每一行都相当于一个套接字,每一列也被称为一个元组,所以一个套接字就是五元组(协议、本地地址、外部地址、状态、PID),有的时候也被叫做四元组,四元组不包括协议。

开启客户端建立连接通信之后
java网络编程Socket实现客户端向服务端发送信息_第6张图片
可以看到开了两个进程,因为我们客户端和服务端都是在一个电脑上跑的,所以会出现这种情况。127.0.0.1是本机的环回地址。

端口号2381和我们程序中拿到的也是一样的。
java网络编程Socket实现客户端向服务端发送信息_第7张图片

扩展

服务端大致流程

在创建ServerSocket 实例的时候,他就已经监听了服务器本地的DEFAULT_PORT端口

serverSocket = new ServerSocket(DEFAULT_PORT);

进入构造函数(CTRL进入)

    public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
    	// new 实例,里面还有一系列逻辑,里面有工厂可以new 实例
 		setImpl();
        //删除了判断输入参数的代码 
        try {
        	//绑定指定端口
            bind(new InetSocketAddress(bindAddr, port), backlog);
        }
        // 删除了出错和异常状态处理的代码
    }

bind 函数

public void bind(SocketAddress endpoint, int backlog) throws IOException {
        // 删除一系列if语句,用于判断业务的前提环境是否异常
        InetSocketAddress epoint = (InetSocketAddress) endpoint;
        try {
			//删除 SecurityManager 安全管理器的检查,检查该线程是否可以监听该端口
				
			//将此Serversocket绑定到指定的本地 IP 地址和端口号
            getImpl().bind(epoint.getAddress(), epoint.getPort());
            //实现对该端口的监听,backlog参数是socket上请求的最大挂起连接数
            getImpl().listen(backlog);
            bound = true; // 标志字段,表示绑定成功
        } 
        //删除了出错和异常状态处理的代码
    }

所以,serversocket一经诞生就已经绑定监听了端口,不绑定监听端口说明没有构造完,这也是他天生的职责。

Socket socket = serverSocket.accept();

监听要与此套接字建立的连接并接受它。 该方法阻塞,直到建立连接

    public Socket accept() throws IOException {
        if (isClosed())
            throw new SocketException("Socket is closed");
        if (!isBound())
            throw new SocketException("Socket is not bound yet");
        // 老规矩,if检查
        Socket s = new Socket((SocketImpl) null);
        //安全管理器的checkAccept方法将使用s.getInetAddress().getHostAddress() s.getPort()和s.getPort()作为其参数调用,以确保允许操作。 这可能会导致 SecurityException。
        implAccept(s);
        return s;
    }

获取向客户端读、写的字符流。(socket的数据肯定是通过运输层协议通信而来的,而网络通信的数据一般为字节流数据,便于网络传输)
InputStreamReader 是字节流通向字符流的桥梁,它将字节流转换为字符流.
OutputStreamWriter是字符流通向字节流的桥梁,它将字符流转换为字节流.

BufferedReader
BufferedWriter
BufferedReader和BufferedWriter 获取到字符流后,可直接缓存,以增加缓冲的方式来提高输入和输出的效率

从read()方法理解,若使用InputStreamReader的read()方法,可以发现存在每2次就会调用一次解码器解码,但若是使用BufferedReader包装InputStreamReader后调用read()方法,可以发现只会调用一次解码器解码,其余时候都是直接从BufferedReader的缓冲区中取字符即可

从read(char cbuf[], int offset, int length)方法理解,若使用InputStreamReader的方法则只会读取leng个字符,但是使用BufferedReader类则会读取读取8192个字符,会尽量提取比当前操作所需的更多字节;

例如文件中有20个字符,我们先通过read(cbuf,0,5)要读取5个字符到数组cbuf中,然后再通过read()方法读取1个字符。那么使用InputStreamReader类的话,则会调用一次解码器解码然后存储5个字符到数组中,然后又调用read()方法调用一次解码器读取2个字符,然后返回1个字符;等于是调用了2次解码器,若使用BufferedReader类的话则是先调用一次解码器读取20个字符到字符缓冲区中,然后复制5个到数组中,在调用read()方法时,则直接从缓冲区中读取字符,等于是调用了一次解码器

因此可以看出BufferedReader类会尽量提取比当前操作所需的更多字节,以应该更多情况下的效率提升,因此在设计到文件字符输入流的时候,我们使用BufferedReader中包装InputStreamReader类即可

 reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
 writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

服务端一行一行的读取读取客户端发送的消息,客户端发送“quit”给服务器时,才表示此客户端要退出

String msg = null;
while ((msg = reader.readLine()) != null) {
    // 读取客户端发送的消息
    System.out.println("客户端["+socket.getInetAddress()+":"+ socket.getPort() + "]: " + msg);

    // 回复客户发送的消息
    writer.write("服务器已收到: " + msg + "\n");
    writer.flush();

    // 查看客户端是否退出
    if (QUIT.equalsIgnoreCase(msg)) {
        System.out.println("客户端["+socket.getInetAddress()+":"+ socket.getPort() + "]已断开连接");
        break;
    }
}

最后close()各种资源

finally {
    try {
        serverSocket.close();
        reader.close();
        writer.close();
        System.out.println("关闭serverSocket");
    } catch (IOException e) {
        e.printStackTrace();
    }

}

客户端大致流程:

socket = new Socket(DEFAULT_SERVER_HOST , DEFAULT_PORT);

构造函数

//创建一个流套接字并将其连接到指定主机上的指定端口号。
public Socket(String host, int port) throws UnknownHostException, IOException{
    this(host != null ? new InetSocketAddress(host, port) :
         new InetSocketAddress(InetAddress.getByName(null), port),
         (SocketAddress) null, true);
}

他的跳转太多太细,我这里放一个执行到connect0()的调用栈,后缀一般带0的都是native方法。
connect0()方法会实现到服务器的连接
我们可以看到:连接业务的开始也是写到socket的构造函数里面的。我们new Socket(DEFAULT_SERVER_HOST , DEFAULT_PORT,里面会有专门的方法比如上面的new InetSocketAddress(host, port)来检查host,port的合法性然后生成/127.0.0.1:8000合法的格式(一个SocketAddress实例),总结起来就是一条业务链上,会横插入很多检查性的或者其他的业务代码,然后代码就会跳来跳去。
java网络编程Socket实现客户端向服务端发送信息_第8张图片
具体怎么连接传递数据已经被封装好了。

连接好了之后,同理获取向服务器读、写的字符流。

reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

写代码获取我们控制台的输入流

consoleReader = new BufferedReader(new InputStreamReader(System.in));

将从控制台获取的数据input,通过writer写入,来与服务器交互信息

while (true) {
    String input = consoleReader.readLine();

    // 发送消息给服务器
    writer.write(input + "\n");
    writer.flush();

    // 读取服务器返回的消息
    String msg = reader.readLine();
    System.out.println(msg);

    // 查看用户是否退出
    if (QUIT.equalsIgnoreCase(input)) {
        break;
    }
}

最后记得close() 各种资源

finally {
    try {
        writer.close(); //关闭之前还会flush一次
        socket.close();
        reader.close();
        consoleReader.close();
        System.out.println("关闭socket");
    } catch (IOException e) {
        e.printStackTrace();
    }
}

References:

  • https://kaven.blog.csdn.net/article/details/104149443
  • https://coding.imooc.com/class/381.html
  • https://www.cnblogs.com/liusxg/p/3917624.html
  • https://blog.csdn.net/jiaomingliang/article/details/45950591
  • https://www.cnblogs.com/winterfells/p/8745297.html
  • https://blog.csdn.net/ai_bao_zi/article/details/81134801
  • https://www.jianshu.com/p/42918db85f19
  • https://mp.weixin.qq.com/s/3Ma4nnkZNWiXacS7Ds2x4Q
  • https://www.pdai.tech/md/framework/tomcat/tomcat-x-design-web-container.html

你可能感兴趣的:(网络)