JavaRush /Java 博客 /Random-ZH /Socket 和 ServerSocket 类,或者“你好,服务器?你能听到我吗?”
Sergey Simonov
第 36 级
Санкт-Петербург

Socket 和 ServerSocket 类,或者“你好,服务器?你能听到我吗?”

已在 Random-ZH 群组中发布
简介:“桌子上有一台电脑,后面有一个编码器……” Socket 和 ServerSocket 类,或者“你好,服务器? 你能听到我吗?”  - 1曾几何时,我的一个同学贴出了他学习Java的另一个成果,以一个新程序的截图的形式。该程序是一个多用户聊天程序。当时我刚刚开始自己​​掌握这种语言编程的旅程,但我明确地对自己说:“我想要它!” 时间过去了,作为加深我的编程知识的一部分,我完成了下一个项目的工作,我想起了那件事,并决定是时候了。不知何故,我纯粹出于好奇已经开始深入研究这个主题,但在我的主要 Java 教科书(这是 Schildt 的完整手册)中,只为 java.net 包提供了 20 页。这是可以理解的——这本书已经很大了。有主类的方法和构造函数表,但仅此而已。当然,下一步是全能的谷歌:无数不同的文章都提出了相同的内容 - 关于套接字的两三个单词,以及一个现成的示例。经典的方法(至少在我的学习风格中)是首先了解我需要从工作工具中获得什么,它们是什么,为什么需要它们,只有这样,如果问题的解决方案不明显,那就选择准备好清单,拧下螺母和螺栓。但我弄清楚了是什么,并最终写了一个多用户聊天。从表面上看,结果是这样的: Socket 和 ServerSocket 类,或者“你好,服务器? 你能听到我吗?”  - 2在这里,我将尝试通过聊天设计的示例让您了解基于 Java 套接字的客户端-服务器应用程序的基础知识。在 Javarash 课程中,您将进行聊天。它将达到一个完全不同的水平,美丽、大型、多功能。但首先,你总是需要打好基础,所以这里我们需要弄清楚这个部分的基础是什么。(如果您发现任何不足或错误,请在PM或文章下的评论中写下)。让我们开始。 第一标题:“房子......” 为了解释服务器和客户端之间的网络连接如何发生,让我们以现在经典的公寓楼为例。假设客户端需要以某种方式与特定服务器建立连接。搜索者需要了解搜索对象的哪些信息?是的,地址。服务器不是云上的神奇实体,因此它必须位于特定的机器上。与房子类比,两个商定的各方应该在那里举行会议。为了在一栋公寓楼中找到对方,仅提供该大楼的地址是不够的;您必须指明会面所在的公寓号。同样,在一台计算机上可以同时有多个服务器,并且为了让客户端联系特定的服务器,他还需要指定进行连接的端口号。所以,地址和端口号。 地址是指互联网空间中机器的标识符。它可以是域名,例如“javarush.ru”,也可以是常规 IP。 港口- 与特定套接字相关联的唯一号码(这个术语将在后面讨论),换句话说,它被某个服务占用,以便可以用来联系它。因此,为了使至少两个对象在其中一个(服务器)的领土上相遇,该区域(服务器)的所有者必须占用其(汽车)上的特定公寓(端口),而第二个对象必须知道会面地点房屋地址(域名或 ip )和公寓号码(端口)。 第二头:认识 Socket 在与网络工作相关的概念和术语中,非常重要的一个是 Socket。它表示连接发生的点。简单地说,套接字连接网络上的两个程序。该类Socket实现了套接字的思想。客户端和服务器将通过其输入/输出通道进行通信: Socket 和 ServerSocket 类,或者“你好,服务器? 你能听到我吗?”  - 3 此类在客户端声明,服务器重新创建它,接收连接信号。这就是在线交流的工作原理。首先,以下是可能的类构造函数Socket
Socket(String Name_хоста, int порт) throws UnknownHostException, IOException
Socket(InetAddress IP-address, int порт) throws UnknownHostException
“host_name” - 表示特定的网络节点、IP 地址。如果套接字类无法将其转换为真实的现有地址,则会抛出异常UnknownHostException。港口就是港口。如果指定端口号为0,系统自己会分配一个空闲端口。如果连接丢失,也可能会发生异常IOException。需要注意的是,第二个构造函数中的地址类型是InetAddress。例如,当您需要指定域名作为地址时,它就可以派上用场。此外,当一个域意味着多个 IP 地址时,InetAddress您可以使用它们来获取它们的数组。然而,它也适用于 IP。您还可以获取主机名、组成 IP 地址的字节数组等。我们将进一步讨论它,但您必须访问官方文档以获取完整的详细信息。当初始化 类型的对象时Socket,它所属的客户端在网络上宣布它想要连接到特定地址和端口号的服务器。以下是该类最常用的方法SocketInetAddress getInetAddress()– 返回包含有关套接字的数据的对象。如果套接字未连接 - null int getPort()- 返回与服务器发生连接的端口 int getLocalPort()- 返回套接字绑定的端口。事实上,客户端和服务器可以在一个端口上“通信”,但它们绑定的端口可以完全不同 boolean isConnected()- 如果连接已建立,则返回 true void connect(SocketAddress address)- 表示一个新连接 boolean isClosed()- 如果套接字关闭,则返回 true boolean isBound()- 返回 true,如果套接字确实绑定到某个地址,则该类Socket实现该接口AutoCloseable,因此可以在try-with-resources. 但是,您也可以使用 close() 以经典方式关闭套接字。 头三:这是一个 ServerSocket 假设我们以类的形式声明了Socket客户端的连接请求。服务器如何猜测我们的愿望?为此,服务器有一个类似 的类 ServerSocket,以及其中的accept() 方法。其构造函数如下所示:
ServerSocket() throws IOException
ServerSocket(int порт) throws IOException
ServerSocket(int порт, int максимум_подключений) throws IOException
ServerSocket(int порт, int максимум_подключений, InetAddress локальный_address) throws IOException
声明时,ServerSocket 不需要指定连接地址,因为通信是在服务器计算机上进行的。仅对于多通道主机,您才需要指定服务器套接字绑定到哪个 IP。 头三.一:拒绝的服务器 由于为程序提供超出其需要的资源既昂贵又不合理,因此在构造函数中ServerSocket要求您声明服务器在运行期间接受的最大连接数。如果未指定,则默认情况下该数字将被视为等于 50。是的,理论上我们可以假设ServerSocket这是同一个套接字,仅适用于服务器。但它扮演着与 class 完全不同的角色Socket。仅在连接创建阶段需要它。创建类型对象后, ServerSocket您需要找出有人想要连接到服务器。这里连接的是accept()方法。目标会一直等待,直到有人想要连接到它,当这种情况发生时,它会返回一个 类型的对象Socket,即重新创建的客户端套接字。现在客户端套接字已在服务器端创建,双向通信就可以开始了。在客户端创建类型对象Socket并使用服务器端重新创建它ServerSocket是连接所需的最低限度。 头四:给圣诞老人的信 Вопрос:客户端和服务器究竟如何通信? Ответ:通过 I/O 流。我们已经拥有什么?带有服务器地址和客户端端口号的套接字,以及相同的东西,这要归功于服务器端的accept()。因此可以合理地假设它们将通过套接字进行通信。为此,有两种方法可以访问 类型的流InputStreamOutputStream对象Socket。他们来了:
InputStream getInputStream()
OutputStream getOutputStream()
由于读取和写入裸字节效率不高,因此可以将流包装在适配器类中,无论是否有缓冲。例如:
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
为了实现双向通信,必须在双方都执行此类操作。现在,您可以使用 in 发送某些内容,并使用 out 接收某些内容,反之亦然。实际上,这实际上是该类的唯一功能Socket。是的,不要忘记flush()方法 BufferedWriter——它刷新缓冲区的内容。如果不这样做,信息将不会被传输,因此也不会被接收。接收线程还要等待行尾指示符——“\n”,否则消息将不会被接受,因为实际上消息还没有完成并且不完整。如果这对你来说不方便,别担心,你可以随时使用PrintWriter需要包装的 class ,指定 true 作为第二个参数,然后从缓冲区中弹出将自动发生:
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
另外,无需指示行尾;此类会为您完成此操作。但是字符串 I/O 是套接字功能的限制吗?不,您想通过套接字流发送对象吗?看在上帝的份上。将它们序列化就可以了:
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
头五:通过互联网进行真实通信 因为为了通过具有真实 IP 地址的真实网络进行连接,您需要拥有一个成熟的服务器,并且因为:
  1. 我们未来的聊天作为一个实用工具,不具备这样的能力。它只能建立连接并接收/发送消息。也就是说,它不具备真正的服务器能力。
  2. 我们的服务器仅包含套接字数据和 I/O 流,无法作为真正的 WEB 或 FTP 服务器工作,那么仅此我们将无法通过 Internet 连接。
此外,我们刚刚开始开发该程序,这意味着它还不够稳定,无法立即与真实网络一起工作,因此我们将使用本地主机作为连接地址。也就是说,理论上,除了通过套接字之外,客户端和服务器仍然不会以任何方式连接,但为了调试程序,它们将位于同一台机器上,而无需通过网络进行真正的联系。为了在构造函数中表明Socket该地址是本地的,有2种方法:
  1. 将“localhost”写入地址参数,表示本地存根。“127.0.0.1”也适合于此 - 这只是存根的数字形式。
  2. 使用 InetAddress:
    1. InetAddress.getByName(null)- null 指向本地主机
    2. InetAddress.getByName("localhost")
    3. InetAddress.getByName("127.0.0.1")
为简单起见,我们将使用字符串类型的“localhost”。但所有其他选择也是可行的。 第六点:是时候进行对话了 因此,我们已经拥有了与服务器实现对话会话所需的一切。剩下的就是将它们放在一起:下面的清单显示了客户端如何连接到服务器,向其发送一条消息,然后服务器使用它作为其参数中的参数来确认它收到了该消息:爪哇”
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    private static Socket clientSocket; // socket for communication
    private static ServerSocket server; // server socket
    private static BufferedReader in; // socket read stream
    private static BufferedWriter out; // socket write stream

    public static void main(String[] args) {
        try {
            try  {
                server = new ServerSocket(4004); // server socket listening on port 4004
                System.out.println("Server is running!"); // server would be nice
                // announce your launch
                clientSocket = server.accept(); // accept() will wait until
                //someone won't want to connect
                try { // after establishing a connection and recreating the socket for communication with the client, you can go
                    // to create I/O streams.
                    // now we can receive messages
                    in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                    // and send
                    out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));

                    String word = in.readLine(); // wait for the client to write something to us
                    System.out.println(word);
                    // without hesitation responds to the client
                    out.write("Hi, this is the Server! I confirm you wrote: " + word + "\n");
                    out.flush(); // push everything out of the buffer

                } finally { // in any case, the socket will be closed
                    clientSocket.close();
                    // streams would also be nice to close
                    in.close();
                    out.close();
                }
            } finally {
                System.out.println("Server closed!");
                    server.close();
            }
        } catch (IOException e) {
            System.err.println(e);
        }
    }
“客户端.java”
import java.io.*;
import java.net.Socket;

public class Client {

    private static Socket clientSocket; // socket for communication
    private static BufferedReader reader; // we need a reader that reads from the console, otherwise
    // do we know what the client wants to say?
    private static BufferedReader in; // socket read stream
    private static BufferedWriter out; // socket write stream

    public static void main(String[] args) {
        try {
            try {
                // address - local host, port - 4004, same as the server
                clientSocket = new Socket("localhost", 4004); // with this line we request
                // the server has access to the connection
                reader = new BufferedReader(new InputStreamReader(System.in));
                // read messages from the server
                in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                // write there
                out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));

                System.out.println("Did you have something to say? Enter it here:");
                // if the connection happened and the threads were successfully created - we can
                // work further and offer the client something to enter
                // if not, an exception will be thrown
                String word = reader.readLine(); // wait for the client to do something
                // will not write to the console
                out.write(word + "\n"); // send a message to the server
                out.flush();
                String serverWord = in.readLine(); // wait for the server to say
                System.out.println(serverWord); // received - display
            } finally { // in any case, you need to close the socket and streams
                System.out.println("The client has been closed...");
                clientSocket.close();
                in.close();
                out.close();
            }
        } catch (IOException e) {
            System.err.println(e);
        }

    }
}
当然,您应该首先启动服务器,因为如果没有任何东西可以连接客户端,那么客户端在启动时将连接到什么?:) 输出将是这样的: /* 你想说点什么吗?在此输入:您好,服务器吗?你能听到我吗?你好,这里是服务器!我确认,您写道: 你好,服务器?你能听到我吗?客户端已关闭... */ 万岁!我们教服务器如何与客户端通信!因此,通信不会发生在两个副本中,而是发生在任意数量的副本中,只需将线程的读取和写入包装在 while (true) 循环中,并根据某个消息指示退出,例如“退出” ,循环被中断,程序结束。 头七:多用户更好。 服务器能听到我们的声音这一点很好,但如果我们能和我们同类的人交流那就更好了。我将在文章末尾附上所有源代码,因此在这里我将展示并不总是很大但很重要的代码片段,如果使用正确,这些代码片段将使得可以构建多用户聊天。因此,我们希望能够通过服务器与其他客户端进行通信。怎么做?显然,由于客户端程序有自己的方法main,这意味着它可以与服务器分开启动,并与其他客户端并行启动。这给我们带来了什么?不知何故,对于每个新连接,服务器不会立即进行通信,而是将该连接写入某种列表并继续等待新连接,并且某种辅助服务与特定的通信进行通信。客户。客户端必须彼此独立地写入服务器并等待响应。线程来救援。假设我们有一个负责记住新连接的类:它应该指定以下内容:
  1. 端口号。
  2. 在其中写入新连接的列表。
  3. 并且ServerSocket,在一个(!)副本中。
public class Server {

    public static final int PORT = 8080;
    public static LinkedList<ServerSomthing> serverList = new LinkedList<>(); // list of all threads

    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(PORT);
            try {
            while (true) {
                // Blocks until a new connection is made:
                Socket socket = server.accept();
                try {
                    serverList.add(new ServerSomthing(socket)); // add a new connection to the list
                } catch (IOException e) {
                    // If it fails, the socket is closed,
                    // otherwise, the thread will close it when it exits:
                    socket.close();
                }
            }
        } finally {
            server.close();
        }
    }
}
好的,现在每个重新创建的套接字都不会丢失,而是会存储在服务器上。更远。每个客户都需要有人倾听他们的声音。让我们使用上一章中的服务器函数创建一个线程。
class ServerSomthing extends Thread {

    private Socket socket; // socket through which the server communicates with the client,
    // except for it - the client and server are not connected in any way
    private BufferedReader in; // socket read stream
    private BufferedWriter out; // socket write stream

    public ServerSomthing(Socket socket) throws IOException {
        this.socket = socket;
        // если потоку ввода/вывода приведут к генерированию исключения, оно пробросится дальше
        in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        start(); // call run()
    }
    @Override
    public void run() {
        String word;
        try {

            while (true) {
                word = in.readLine();
                if(word.equals("stop")) {
                    break;                }
                for (ServerSomthing vr : Server.serverList) {
                    vr.send(word); // send the received message with
                   // bound client to everyone else including him
                }
            }

        } catch (IOException e) {
        }
    }

    private void send(String msg) {
        try {
            out.write(msg + "\n");
            out.flush();
        } catch (IOException ignored) {}
    }
}
因此,在服务器线程的构造函数中,必须初始化一个套接字,线程将通过该套接字与特定客户端进行通信。还有 I/O 线程,以及直接从构造函数启动线程所需的所有其他内容。好的,但是当服务器线程从客户端读取消息时会发生什么?只发回给您的客户?不太有效。我们正在进行多用户聊天,因此我们需要每个连接的客户端接收一个人写的内容。您需要使用与其客户端关联的所有服务器线程的列表,并将每条消息发送到特定线程,以便将其发送到其客户端:
for (ServerSomthing vr : Server.serverList) {
    vr.send(word); // send the received message
    // from the linked client to everyone else, including him
}
private void send(String msg) {
    try {
        out.write(msg + "\n");
        out.flush();
    } catch (IOException ignored) {}
}
现在所有客户都会知道其中一位所说的话!如果您不想将消息发送给发送它的人(他已经知道他写了什么!),只需在迭代线程时指定在处理对象时,循环this将移至下一个元素而不执行对其采取任何行动。或者,如果您愿意,可以向客户端发送一条消息,说明消息已成功接收和发送。现在服务器一切都清楚了。让我们转向客户,或者更确切地说,转向客户!一切都是一样的,与上一章中的客户端类比,只有在创建实例时,您才需要创建构造函数中所需的所有内容,如本章中的服务器所示。但是,如果在创建客户端时,他还没有时间输入任何内容,但有些内容已经发送给他怎么办?(例如,那些已经连接到他之前的聊天记录的人的通信历史)。因此,处理发送消息的周期必须与从控制台读取消息并发送到服务器转发给其他人的周期分开。线程再次拯救了我们。将客户端创建为线程是没有意义的。更方便的做法是在run方法中创建一个带循环的线程来读取消息,同样类推,这样写:
// thread reading messages from the server
private class ReadMsg extends Thread {
    @Override
    public void run() {

        String str;
        try {
            while (true) {
                str = in.readLine(); // waiting for a message from the server
                if (str.equals("stop")) {

                    break; // exit the loop if it's "stop"
                }
                            }
        } catch (IOException e) {

        }
    }
}
// thread sending messages coming from the console to the server
public class WriteMsg extends Thread {

    @Override
    public void run() {
        while (true) {
            String userWord;
            try {
               userWord = inputUser.readLine(); // messages from the console
                if (userWord.equals("stop")) {
                    out.write("stop" + "\n");
                    break; // exit the loop if it's "stop"
                } else {
                    out.write(userWord + "\n"); // send to the server
                }
                out.flush(); // clean up
            } catch (IOException e) {

            }

        }
    }
}
在客户端构造函数中,您只需要启动这些线程。如果客户要离开,如何正确关闭资源?我需要关闭服务器线程资源吗?为此,您很可能需要创建一个单独的方法,在退出消息循环时调用该方法。在那里您需要关闭套接字和 I/O 流。特定客户端的相同会话结束信号必须发送到其服务器线程,该服务器线程必须对其套接字执行相同的操作,并将其自身从主服务器类中的线程列表中删除。 第八点:完美无极限 您可以不断地发明新功能来改进您的项目。但是到底应该向新连接的客户端传输什么内容呢?我认为最后十件事发生在他到来之前。为此,您需要创建一个类,其中任何服务器线程的最后一个操作都将输入到声明的列表中,如果列表已经满(即已经有 10 个),则删除第一个并添加最后一个来的。为了让新连接能够接收该列表的内容,在创建服务器线程时,需要在输出流中将它们发送到客户端。怎么做?例如,像这样:
public void printStory(BufferedWriter writer) {
// ...
}
服务器线程已经创建了流,并且可以将输出流作为参数传递。接下来,您只需在一个搜索周期中将需要传输的所有内容传递给新客户端即可。 结论: 这只是基础知识,在创建真正的应用程序时,这种聊天架构很可能不起作用。该程序是出于教育目的而创建的,在其基础上,我展示了如何使客户端与服务器通信(反之亦然),如何对多个连接执行此操作,当然,还有如何在套接字上组织它。下面重新整理了源码,并附上了正在分析的程序的源代码。这是我第一次写文章)感谢您的关注:)
  1. 《Java Enterprise 中的思考》,作者:Bruce Eckel 等人。阿尔。2003年
  2. Java 8,完整指南,Herbert Schildt,第 9 版,2017 年(第 22 章)
  3. Java 中的套接字编程有关套接字的文章
  4. 官方文档中的socket
  5. 官方文档中的ServerSocket
  6. GitHub 上的资源
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION