Java|Socket编程指南

Java|Socket编程指南,第1张

概述

Socket编程是指编写在多台计算机上执行的程序,其中的设备都使用网络相互连接

Socket常用的通信协议有UDP和TCP,本文主要介绍通过TCP/IP网络协议进行Socket编程

Socket通信流程

  • 服务端和客户端初始化 socket,得到文件描述符;
  • 服务端调用 bind,将绑定在 IP 地址和端口;
  • 服务端调用 listen,进行监听;
  • 服务端调用 accept,等待客户端连接;
  • 客户端调用 connect,向服务器端的地址和端口发起连接请求;
  • 服务端 accept 返回用于传输的 socket 的文件描述符;
  • 客户端调用 write 写入数据;服务端调用 read 读取数据;
  • 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭;

服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。

简单示例

Socket是网络上不同计算机运行的两个程序之间双向通信链路的一个端点。Socket需要绑定端口号,一遍传输层可以标识数据要发送到的应用程序

服务端

服务端会用到两个socket,一个叫作监听 socket,一个叫作已完成连接 socket

目前的服务器不能保证通信的连续性,它会在发送完消息后关闭连接

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

// socket服务端
public class Server {
    private ServerSocket serverSocket;
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void start(int port){
        try{
            // (监听socket)
            // 绑定指定端口,使服务器的Socket在指定端口号上运行
            serverSocket = new ServerSocket(port);

            // (已连接socket)
            // 服务器遇到accept进入阻塞,等待客户端发出连接
            // 连接成功后,服务器将获得绑定到同一本地端口6666的新socket,用于传输数据
            clientSocket = serverSocket.accept();

            // 输出流,可发送消息到客户端
            out = new PrintWriter(clientSocket.getOutputStream(), true);
            // 输入流,可接收客户端消息
            in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

            String greeting = in.readLine();

            if(greeting.equals("hello server")){
                out.println("hello client");
            }
            else{
                out.println("unrecognised greeting");
            }

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


    }

    public void stop(){
        try{
            in.close();
            out.close();
            clientSocket.close();
            serverSocket.close();

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


    }

    public static void main(String[] args) {
        Server server = new Server();
        // 开启服务器
        server.start(6666);
    }

}

客户端

客户端只需要创建一个socket以保持连接,最终客户端的输入流连接到服务端的输出流,服务器的输入流连接到客户端的输出流

import com.sun.javafx.iio.ios.IosDescriptor;

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

public class Client {
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void startConnection(String ip, int port){

        try{
            // 客户端需要知道服务端的ip和其正在监听的端口号,才能发起连接
            // 服务器接收连接后创建客户端socket
            clientSocket = new Socket(ip, port);

            // 获取socket的输入输出流,以便与服务端通信
            out = new PrintWriter(clientSocket.getOutputStream(), true);
            in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        }
        catch (IOException e){
            e.printStackTrace();
        }



    }

    public String sendMessage(String msg){
		
        // 客户端输出请求消息
        out.println(msg);
        String resp = null;
        try {
            // 客户端接收响应消息
            resp = in.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return resp;

    }

    public void stopConnection(){

        try {
            in.close();
            out.close();
            clientSocket.close();
        }
        catch (IOException e){
            e.printStackTrace();
        }


    }

}

测试

先手动启动服务端,然后运行以上测试案例即可完成一次连接和一次消息发送

import org.junit.Test;
public class HelloTest {
    @Test
    public void hello(){
        Client client = new Client();
        // 客户端对服务端发起连接
        client.startConnection("127.0.0.1", 6666);
        // 客户端发送消息到服务端并接收响应结果
        String response = client.sendMessage("hello server");
        System.out.println(response);

    }

}

若启动服务端时出现以下报错,是出现了端口占用,可以修改端口也可以关闭占用端口的进程

Windows下使用命令行关闭占用端口的进程

// 参看端口号含6666的条目
netstat -ano|findstr "6666"

// 根据pid查询对应的应用程序
tasklist|findstr "1828"

// 杀死进程
taskkill /f /pid 1828
持续连接优化

在前一个案例中,服务器会阻塞直到客户端连接它。在单个消息后,连接就会关闭,客户端和服务端无法持续沟通,因此仅仅会出现在ping请求中

如果要实现一个聊天服务器,客户端和服务端之间就需要连续的来回通信

服务端

在优化中我在服务端创建一个while循环来连续观察传来消息的服务器输入流

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class EchoServer {
    private ServerSocket serverSocket;
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void start(int port){
        try{
            // (监听socket)
            // 绑定指定端口,使服务器的Socket在指定端口号上运行
            serverSocket = new ServerSocket(port);

            // (已连接socket)
            // 服务器遇到accept进入阻塞,等待客户端发出连接
            // 连接成功后,服务器将获得绑定到同一本地端口4444的新socket,用于传输数据
            clientSocket = serverSocket.accept();

            // 输出流,可发送消息到客户端
            out = new PrintWriter(clientSocket.getOutputStream(), true);
            // 输入流,可接收客户端消息
            in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

            String inputLine;
            // while循环连续观察从客户端传来消息的服务器输入流
            // 直到读取到exit断开连接
            while ((inputLine = in.readLine()) != null){
                if(inputLine.equals("exit")){
                    out.println("goodbye");
                    break;
                }
                out.println(inputLine.replace("req", "res"));

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


    public void stop(){
        try{
            in.close();
            out.close();
            clientSocket.close();
            serverSocket.close();

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


    }

    public static void main(String[] args) {
        EchoServer server = new EchoServer();
        // 开启服务器
        server.start(4444);
    }

}

客户端

客户端不需要进行优化修改,这里为了方区分创建一个新的类EchoClient

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

public class EchoClient {
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void startConnection(String ip, int port){

        try{
            // 客户端需要知道服务端的ip和其正在监听的端口号,才能发起连接
            // 服务器接收连接后创建客户端socket
            clientSocket = new Socket(ip, port);

            // 获取socket的输入输出流,以便与服务端通信
            out = new PrintWriter(clientSocket.getOutputStream(), true);
            in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        }
        catch (IOException e){
            e.printStackTrace();
        }



    }


    public String sendMessage(String msg){
		
        // 客户端输出请求消息
        out.println(msg);
        String resp = null;
        try {
            // 客户端接收响应消息
            resp = in.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return resp;

    }


    public void stopConnection(){

        try {
            in.close();
            out.close();
            clientSocket.close();
        }
        catch (IOException e){
            e.printStackTrace();
        }


    }

}
测试

在初始示例中,我们只在服务器关闭连接之前进行一次通信。现在,我们发送一个终止信号,以便在会话完成时告诉服务器,以此关闭服务器的socket进程

开启EchoServer服务器运行以下测试案例

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class EchoTest {

    private EchoClient client = new EchoClient();

    @Before
    public void setup(){
        client.startConnection("127.0.0.1", 4444);
    }

    @After
    public void tearDown(){
        client.stopConnection();
    }

    @Test
    public void echo(){
        String resp1 = client.sendMessage("req:hello");
        String resp2 = client.sendMessage("req:world");
        String resp3 = client.sendMessage("exit");

        System.out.println(resp1);
        System.out.println(resp2);
        System.out.println(resp3);
    }

}

  • @BeforeClass – 表示在类中的任意public static void方法执行之前执行
  • @AfterClass – 表示在类中的任意public static void方法执行之后执行
  • @Before – 表示在任意使用@Test注解标注的public void方法执行之前执行
  • @After – 表示在任意使用@Test注解标注的public void方法执行之后执行
  • @Test – 使用该注解标注的public void方法会表示为一个测试方法
多客户端优化

在实际情况中,服务端常常要处理多个客户端的请求,为此我们要在服务端为每一个客户端请求创建一个新的socket线程,即提供服务的客户端数将等于服务端正在运行的线程数

服务端

仍然用一个监听socket在主线程监听端口,而需要多线程存储已连接socket以保持与多个客户端的连接

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

public class EchoMultiSever {
    private ServerSocket serverSocket;

    public void start(int port){

        try {
            // 仍然使用一个socket在主线程中监听端口
            serverSocket = new ServerSocket(port);
            while (true){
                // 每次循环中,accept会阻塞调用,直到新的客户端调用
                // 连接成功后,EchoMultiServer会将已连接的socket委托给 EchoClientHandler
                new EchoClientHandler(serverSocket.accept()).start();
            }
        }
        catch (IOException e){
            e.printStackTrace();
        }

    }

    public void stop(){
        try{
            serverSocket.close();
        }
        catch (IOException e){
            e.printStackTrace();
        }
    }

    // 创建一个单独的线程EchoClientHandler
    // 保存已连接的socket与客户端交流
    public static class EchoClientHandler extends Thread{
        private Socket clientSocket;
        private PrintWriter out;
        private BufferedReader in;

        public EchoClientHandler(Socket socket){
            this.clientSocket = socket;
        }

        // 线程执行start直到运行run方法,与目标客户端进行交流
        // 其内部发生的情况与EchoSever相同
        public void run(){

            try{

                out = new PrintWriter(clientSocket.getOutputStream(), true);
                in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

                String inputLine;
                while ((inputLine = in.readLine()) != null){
                    if(inputLine.equals("exit")){
                        out.println("bye");
                        break;
                    }
                    out.println(inputLine.replace("req", "res"));
                }

                in.close();
                out.close();
                clientSocket.close();

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


        }


    }

    public static void main(String[] args) {
        EchoMultiSever server = new EchoMultiSever();
        // 开启服务器
        server.start(5555);
    }


}

客户端

客户端不需要进行优化修改,与上面的相同

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

public class EchoMultiClient {
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void startConnection(String ip, int port){

        try{
            // 客户端需要知道服务端的ip和其正在监听的端口号,才能发起连接
            // 服务器接收连接后创建客户端socket
            clientSocket = new Socket(ip, port);

            // 获取socket的输入输出流,以便与服务端通信
            out = new PrintWriter(clientSocket.getOutputStream(), true);
            in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        }
        catch (IOException e){
            e.printStackTrace();
        }



    }

    public String sendMessage(String msg){

        out.println(msg);
        String resp = null;
        try {
            resp = in.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return resp;

    }

    public void stopConnection(){

        try {
            in.close();
            out.close();
            clientSocket.close();
        }
        catch (IOException e){
            e.printStackTrace();
        }


    }

}

测试

测试类中需要发起多个客户端请求

运行EchoMultiSever后,运行以下案例

import org.junit.Test;

// 测试方法开启多个客户端请求
public class EchoMultiTest {
    @Test
    public void buildClient1(){
        EchoClient client1 = new EchoClient();
        client1.startConnection("127.0.0.1", 5555);
        String resp1 = client1.sendMessage("req:hello");
        String resp2 = client1.sendMessage("req:world");
        String resp3 = client1.sendMessage("exit");

        System.out.println(resp1);
        System.out.println(resp2);
        System.out.println(resp3);
    }

    @Test
    public void buildClient2(){
        EchoClient client2 = new EchoClient();
        client2.startConnection("127.0.0.1", 5555);
        String resp1 = client2.sendMessage("req:hello");
        String resp2 = client2.sendMessage("req:world");
        String resp3 = client2.sendMessage("exit");

        System.out.println(resp1);
        System.out.println(resp2);
        System.out.println(resp3);
    }

    @Test
    public void buildClient3(){
        EchoClient client3 = new EchoClient();
        client3.startConnection("127.0.0.1", 5555);
        String resp1 = client3.sendMessage("req:hello");
        String resp2 = client3.sendMessage("req:world");
        String resp3 = client3.sendMessage("exit");

        System.out.println(resp1);
        System.out.println(resp2);
        System.out.println(resp3);
    }

}

参考文章

Java 套接字

TCP/IP图解

欢迎分享,转载请注明来源:内存溢出

原文地址:https://www.54852.com/langs/798859.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-05-06
下一篇2022-05-06

发表评论

登录后才能评论

评论列表(0条)

    保存