Java:TCP编程与Socket

在开发网络应用程序时,我们会遇到Socket这个概念,它是一个抽象概念,一个应用程序通过一个Socket来建立一个远程连接,而Socket内部通过TCP/IP协议数据传输到网络

Socket、TCP部分IP的功能都是由OS提供的,不同的编程语言只是提供了对OS调用的简单封装。例如,Java提供的几个Socket相关的类就封装了OS提供的接口。

 为什么需要Socket进行网络通信?因为仅仅通过IP地址进行通信是不够的,因为同一台计算机同一时间会运行多个网络应用程序,例如浏览器、QQ、邮件客户端等。当OS收到一个数据包时,如果只有IP地址,它没法判断应该发给哪个应用程序,所以,OS抽象出Socket接口,每个应用程序需要各自对应到不同的Socket,数据包才能根据Socket正确地发到对应的应用程序。

一个Socket就是由IP端口号(范围0~65535)组成,可以把Socket简单理解为IP+端口端口号OS分配,范围0~65535,其中,0~1023属于特权端口,需要管理员权限1024~65535的端口可以由任意用户的应用程序打开:

  •  101.202.99.2:1201
  •  101.202.99.2:1304
  •  101.202.99.2:15000

使用Socket进行网络编程,本质上就是两个进程间的网络通信。其中一个进程必须充当服务器端,它主动监听某个指定端口,另一个进程必须充当客户端,它必须主动连接服务器的IP指定端口,如果连接成功,Server和Client就成功地建立了一个TCP连接,双方后续就可以随时收发数据。

因此,Socket连接成功,就说明Server与Client之间建立了连接:

  • 对Server来说,它的Socket是指定IP端口
  • 对Client来说,它的Socket是它所在计算机的IP和一个OS随机分配的端口

服务器端(Server)

要使用Socket编程,我们首先要编写Server端程序。Java标准库(java.net.ServerSocket)提供了ServerSocket来实现对指定的IP端口的监听。

ServerSocket的典型实现代码如下:

复制代码
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class Server {
    public static void main(String [] args) throws IOException{
     //服务器端Socket,需指定监听端口,但无需指定IP ServerSocket ss
= new ServerSocket(6666); System.out.println("server is running..."); while(true){ Socket sock = ss.accept(); System.out.println("connected from "+sock.getRemoteSocketAddress()); Thread t = new Handler(sock); t.start(); } } } class Handler extends Thread{ Socket sock; public Handler(Socket sock){ this.sock=sock; } @Override public void run(){ try(InputStream input = this.sock.getInputStream()){ try(OutputStream output = this.sock.getOutputStream()){ handle(input,output); } } catch(Exception e){ try{ this.sock.close(); }catch(IOException ioe){ } System.out.println("client disconnected"); } } private void handle(InputStream input,OutputStream output) throws IOException{ var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8)); var reader = new BufferedReader(new InputStreamReader(input,StandardCharsets.UTF_8)); writer.write("hello\n"); writer.flush(); while(true){ String s=reader.readLine(); if(s.equals("bye")){ writer.write("bye\n"); writer.flush(); break; } writer.write("ok: "+s+"\n"); writer.flush(); } } }
复制代码

解析

服务器端通过代码:

ServerSocket ss = new ServerSocket(6666);

监听指定端口6666。这里我们没有指定IP地址,表示在计算机的所有网络接口上进行监听。

如果ServerSocket监听成功,我们就用一个无限循环来处理Client的连接:

while(true){
    Socket sock = ss.accpet();
    Thread t = new Handler(sock);
    t.start();
}

代码ss.accept()表示每当有新Client接入后,就返回一个Socket实例,这个Socket实例就是用来和刚连接的Client进行通信的。由于Client很多,要实现并发处理,我们就必须为每个新Socket创建一个新线程来处理,这样,主线程的作用就是接收新的连接,每当收到新连接后,就创建一个新线程进行处理。

我们之前在2021.10.9:线程池 - ShineLe - 博客园介绍过线程池,这里也完全可以用线程池来处理Client连接,可以大大提高运行效率。

如果没有Client连接进来,accept()方法会阻塞一直等待;如果有多个Client同时连接进来,ServerSocket会把连接扔到队列中,然后一个一个处理

对于Java程序而言,只需要通过循环不断调用accept()就可以获取新的连接

客户端(Client)

相比服务器端,客户端程序就简单很多。一个典型的客户端程序如下:

复制代码
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class Client {
    public static void main(String[]args) throws IOException {
        Socket sock = new Socket("localhost",6666);
        try(InputStream input = sock.getInputStream()){
            try(OutputStream output = sock.getOutputStream()){
                handle(input,output);
            }
        }
        sock.close();
        System.out.println("disconnected.");
    }
    private static void handle(InputStream input,OutputStream output) throws IOException{
        var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
        var reader = new BufferedReader(new InputStreamReader(input,StandardCharsets.UTF_8));
        Scanner scanner = new Scanner(System.in);
        System.out.println("[server] "+reader.readLine());
        while(true){
            System.out.print(">>> ");//打印提示
            String s = scanner.nextLine();//读取一行输入
            writer.write(s);
            writer.newLine();
            writer.flush();
            String resp = reader.readLine();
            System.out.println("<<< "+resp);
            if(resp.equals("bye")){
                break;
            }
        }
    }
}
复制代码

客户端通过:

Socket sock = new Socket("localhost",6666);

连接到Server;注意到上述代码的Server地址为"localhost",表示本机地址端口号6666。如果连接成功,将返回一个Socket实例,用于后续通信。

 

Socket流

当Socket连接创建成功后,无论Server还是Client,我们都使用Socket实例进行网络通信

因为TCP协议是一种基于的协议,因此,Java标准库使用InputStreamOutputStream来封装Socket数据流,这样我们使用Socket的流,和普通IO流类似:

//用于读取网络数据
InputStream in = sock.getInputStream();
//用于写入网络数据
OutputStream out = sock.getOutputStream();

那么,为什么写入网络数据时,要用flush()方法?

如果不调用flush(),我们会发现,ClientServer都收不到数据,这并不是Java标准库的设计问题,而是我们以的形式写入数据的时候,并不是一写入就立刻发送到网络,而是先写入内存缓冲区,直到缓冲区满了之后,才会一次性真正发送到网络,这样设计的目的是为了提高传输效率。

如果缓冲区数据很少,而我们又想强制把这些数据发送到网络,就必须调用flush()强制把缓冲区的数据发送出去。

 

小结

使用Java进行TCP编程时,需要使用Socket模型:

  • ServerServerSocket监听指定端口
  • ClientSocket( ip , port )连接Server
  • Serveraccept()接收Connect并返回Socket
  • 双方通过Socket打开InputStream/OutputStream 读写数据;
  • Server通常用多线程同时处理多个Server连接,利用线程池可以大幅度提升效率;
  • flush()用于强制输出缓冲区网络

总结

1、应用程序通过Socket建立远程连接Socket内部通过TCP/IP协议数据传递给网络。

2、Socket = IP + 端口。端口范围0~65535,其中0~1023特权端口,需要管理员权限;1024~65535可供任意用户的应用程序使用。

3、使用Socket进行网络编程的本质,是两个进程间的网络通信。这两个进程分别称为服务器客户端

  • 服务器主动监听某个端口;
  • 客户端主动连接服务器的IP与端口。

连接建立成功后,服务器与客户端间就建立了TCP连接,双方可以随时收发数据。

对于服务器和客户端而言,双方对于连接的持有项是不同的:

  • 服务器持有的Socket是指定的IP与端口,这个是不变的,这也是监听的含义;
  • 客户端持有的Socket则是它的IP与OS随机分配的端口。与服务器持有的恒定端口相区别。

4、服务器端代码

Java标准库(java.net.ServerSocket)提供了ServerSocket来实现对指定IP与端口监听

①导入相关包

import java.net.ServerSocket;
import java.net.Socket;

②创建服务器类,对这个类的要求是:

  • A、这个类中要有一个main方法,在main方法中监听指定端口
  • B、要有一个无限循环的代码块,表示服务器不断监听
  • C、要是监听到了客户端请求(表现为accept客户端的Socket),就为其分配一个用于处理具体事务的线程(在无限循环的代码块中),启动线程处理具体事务
复制代码
public class Server{
    public static void main(String[]args) throws IOException{
            //A、监听端口6666
            ServerSocket ss = new ServerSocket(6666);
            ....
       //B、不断监听
            while(true){
                //B、通过accept()监听到客户端发来的请求
          //没有听到就会阻塞并一直等待
                Socket sock = ss.accept();
                ...
          //C、用该客户端Socket分配一个线程,处理具体事务
                //Handler是一个处理具体事务的线程类,从Thread继承而来
                Thread t = new Handler(sock);
                //启动线程,处理相关事务
                t.start();
            }
    }
}
复制代码

③对处理具体事务线程类(如上文的Handler)的编写,对这个线程的要求:继承自Thread,且需要有一个Socket成员变量来接受实际客户端的端口

5、客户端代码编写

①相关包的导入,与服务器端的区别在于不用导入ServerSocket

import java.net.Socket;

②创建客户端类

对这个类的要求是:

  • A、需要一个main方法,在mainnew一个客户端Sokcet,需要同时指定IP与端口,且端口与服务器端一致,如果new成功(表明服务器与客户端间建立了连接),将得到一个Socket实例,用于后续通信
  • B、客户端无需无限循环代码块,也不需要独立线程,通常只发送一两次请求,通过socket.getInputStream()获取客户端发送的请求,通过socket.getOutputStream()获取服务器的响应
  • C、客户端Socket在请求服务器处理完毕后要销毁close()
复制代码
public class Client {
    public static void main(String[]args) throws IOException {
    //A
        Socket sock = new Socket("localhost",6666);
    //B
        try(InputStream input = sock.getInputStream()){
            try(OutputStream output = sock.getOutputStream()){
                handle(input,output);
            }
        }
    //C
        sock.close();
        System.out.println("disconnected.");
    }
复制代码

③对处理具体事务的方法的实现(上文handle

不用再新建线程类,在这个方法中,需要持有两个变量分别代表客户端发送收到的服务器端数据,在上文代码中用InputStream inputOutputStream output表示。

6、利用Socket实现网络通信

接着第5点的第③小点说,当Socket连接创建完成后(客户端new Socket之后,收到服务器端响应——getInputStream、getOutputStream都有回应),ServerClient间通过Socket实例进行通信

Java标准库用InputStreamOutputStream封装Socket的数据流,我们在使用Socket的流时和普通的IO流类似:

//用于读取网络数据
InputStream in = sock.getInputStream();
//用于写入网络数据
OutputStream out = sock.getOutputStream();

写入数据时,需要调用flush()方法,及时将写入的数据发送到网络

posted @   ShineLe  阅读(550)  评论(0编辑  收藏  举报
编辑推荐:
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
历史上的今天:
2020-10-21 2020.10.20 利用POST请求模拟登录知乎
点击右上角即可分享
微信分享提示