【网络编程】Socket套接字;UDP数据报套接字编程;TCP流套接字编程

文章目录

    • 1. 什么是网络编程
    • 2. 网络编程中的基本概念
    • 3. Socket套接字
    • 4 UDP数据报套接字编程
      • 4.1 客户端服务器交互流程
      • 4.2 UDP版本的回显服务
      • 4.3 英译汉服务
    • 5. TCP流套接字编程
      • 5.1 TCP版本的回显服务
      • 5.2 服务器优化
      • 5.3 英译汉服务

1. 什么是网络编程

网络编程是指网络上的主机,通过不同的进程,以代码编程的方式实现网络通信(网络数据传输)

当然我们只需要满足进程不同就行,所以即便是同一个主机,只要是不同进程,基于网络来传输数据也属于网络编程。

操作系统把网络编程的一些相关操作封装起来,提供了一组API供程序员来调用 – Socket(套接字)API

2. 网络编程中的基本概念

发送端和接收端

在一次网络数据传输时:
发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
收发端:发送端和接收端两端,也简称为收发端。

发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念

请求和响应

一般来说,获取一个网络资源,涉及到两次网络数据传输:
第一次:请求数据的发送
第二次:响应数据的发送。

客户端和服务器

服务器:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。

客户端:获取服务的一方进程,称为客户端。

常见的客户端服务器模型

  1. 客户端先发送请求到服务器
  2. 服务器根据请求数据,执行相应的业务处理
  3. 服务器返回响应:发送业务处理结果
  4. 客户端根据响应数据,展示处理结果

3. Socket套接字

Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程

Socket套接字针对传输层协议主要分为两种:流套接字,数据报套接字

流套接字:使用传输层TCP协议

TCP :传输控制协议

特点

  1. 有连接
  2. 可靠传输
  3. 面向字节流
  4. 全双工通信

对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。

对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情
况下,是无边界的数据,可以多次发送,也可以分开多次接收。

数据报套接字:使用传输层UDP协议

UDP:用户数据报协议

特点:

  1. 无连接
  2. 不可靠传输
  3. 面向数据报
  4. 全双工通信

对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。

理解有连接和无连接

有连接类似于打电话,我们和朋友打电话时,双方能听到声音并说话的前提是我电话拨通了,他也接电话了,我俩之间建立连接了,才能说话。

无连接类似于发微信,给朋友发微信不需要建立连接,只需要给他发出去就行了,至于对方什么时候看到就不确定了。

理解可靠传输和不可靠传输

可靠传输和不可靠传输并不是说发送的数据就一定能被对方收到,

可靠传输指的是发送方能知道数据是否被对方接收到了

不可靠传输指的是发送方不能知道数据是否被对方收到了

面向字节流和面向数据报

面向字节流:如果要发送100个字节,可以一次发1个字节,发送100次;也可以一次发10个字节,发送10次。可以非常灵活的完成这里的发送,接收也是如此,发送和接收的最小单位是字节

面向数据报:以一个一个的数据报为基本单位,数据报多大不确定,不同的协议有不同的约定。并且发送的时候,一次至少发送一个数据报,接收的时候,一次至少接收一个数据报

全双工

全双工:双向通信,A和B可以同时发送数据和接收数据

半双工:单向通信,同一时间只能有一方发送数据,要么A给B发,要么B给A发

4 UDP数据报套接字编程

DatagramSocket API

DatagramSocket 描述一个UDPSocket对象,用于发送和接收UDP数据报。

Java标准库中的DatagramSocket对象 就是表示一个Socket文件,socket本质上是一个文件描述符表,网络编程主要是通过网卡来实现的,读取socket文件中的数据,就是读取网卡,向socket文件中写入数据就是写入网卡,我们可以理解为socket文件就是一个遥控器,用来操作网卡,使我们实现网络编程

DatagraSocket构造方法

方法名 说明
DatagramSocket() 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端)
DatagramSocket(int port) 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务器)

DatagramSocket方法

方法名 说明
void receive(DatagramPacket p) 从此套接字接收数据报,如果没有接收到数据报,就会阻塞等待
void send(DatagramPacket p) 从此套接字发送数据报包,不会阻塞等待,直接发送
void close() 关闭此数据报套接字

DatagramPacket API

DatagramPacket 描述一个UDP数据报

UDP面向数据报,发送接收数据,就是以DatagramPacket对象为单位进行的

DatagramPacket 构造方法

方法名 说明
DatagramPacket(byte[] buffer, int length) 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buffer)中,接收指定长度(第二个参数length)
DatagramPacket(byte[] buffer, int offset, int length,SocketAddress address) 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buffer)中,从0到指定长度(第二个参length)。address指定目的主机的IP和端口号

DatagramPacket 方法

方法名 说明
InetAddress getAddress() 从接收的数据报中,获得发送端的主机IP地址,或从发送的数据报中,获得接收端的主机IP地址
int getPort() 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号
byte[] getData() 获取数据报中的数据

构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建

InteSocketAddress API

使用InetSocketAddress(InetAddress addr,int port)方法创建一个Socket地址,包含IP地址和端口号

4.1 客户端服务器交互流程

  1. 客户端给服务器发送请求
  2. 服务器读取请求并解析
  3. 服务器根据请求计算响应
  4. 服务器构造响应并返回
  5. 客户端获得响应并显示

4.2 UDP版本的回显服务

回显服务就是客户端发送什么,服务器就回应什么

服务器:

public class Server {
    private DatagramSocket socket = null;

    public Server(int port) throws SocketException {
        this.socket = new DatagramSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            String response = process(request);
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),0,response.getBytes().length,
                                                                requestPacket.getSocketAddress());
            socket.send(responsePacket);
            String log = String.format("[%s;%d],res: %s ,reps: %s",
                    requestPacket.getAddress().toString(),
                    requestPacket.getPort(),
                    request,response);
            System.out.println(log);
        }
    }

    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        Server server = new Server(9050);
        server.start();
    }
}

客户端

public class Client {
    private DatagramSocket socket = null;
    private String serverIp;
    private int serverPort;

    public Client(String serverIp, int serverPort) throws SocketException {
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        socket = new DatagramSocket();
    }
    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("->");
            String request = scanner.nextLine();
            if(request.equals("exit")){
                System.out.println("exit");
                return;
            }
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                                                                InetAddress.getByName(serverIp),serverPort);
            socket.send(requestPacket);
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());
            String log = String.format("res: %s ; reps: %s",request,response);
            System.out.println(log);
        }
    }

    public static void main(String[] args) throws IOException {
        Client client = new Client("127.0.0.1",9050);
        client.start();
    }
}

具体分析

服务器中最核心的部分就是process方法,根据请求计算响应,此处我们实现的是回显服务,下面我们做一个简单的英译汉服务器

4.3 英译汉服务

public class DictServer {
    private DatagramSocket socket = null;
    private HashMap<String,String> map = new HashMap<>();
    public DictServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
        //对hashmap进行初始化构造,可以构造多个数据
        map.put("cat","猫");
        map.put("dog","狗");
        map.put("hello","你好");
    }
    public void start() throws IOException {
        System.out.println("服务器启动");
        while(true){
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            String response = process(request);
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                                            requestPacket.getSocketAddress());
            socket.send(responsePacket);
            String log = String.format("[%s,%d] req: %s ; resp: %s",
                                        requestPacket.getAddress().toString(),
                                        requestPacket.getPort(),
                                        request,response);
            System.out.println(log);
        }
    }

    private String process(String request) {
        //根据用户请求计算响应,英译汉的核心就是查表,构造方法中在map中加入了3个数据
        //此处使用getOrDefault,key存在就返回对应的value。不存在就返回"我不会"
        return map.getOrDefault(request,"我不会");
    }

    public static void main(String[] args) throws IOException {
        DictServer server = new DictServer(9090);
        server.start();
    }
}

除了在process中处理业务的逻辑和回显服务器不同之外,其他部分的代码都基本相同

5. TCP流套接字编程

ServerSocket API

ServerSocket 是创建TCP服务端Socket的API

ServerSocket构造方法:ServerSocket(int port),创建一个服务端流套接字Socket,并绑定到指定端口

ServerSocket方法

方法名 说明
Socket accept() 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该对Socket建立与客户端的连接,否则阻塞等待
void close() 关闭此套接字

Socket API

Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。

Socket构造方法:Socket(String hose,int port)创建一个客户端流套接字Socket,并与对应IP的主机上对应端口的进程建立连接,也就是与想要交互的服务器建立连接

Socket 方法

方法名 说明
InetAddress getInetAddress() 返回套接字所连接的地址
InputStream getInputStream() 返回此套接字的输入流
OutputStream getOutputStream() 返回此套接字的输出流

5.1 TCP版本的回显服务

服务器

public class Server {
    private ServerSocket listenSocket = null;
    public Server(int port) throws IOException {
        listenSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动");
        while(true){
            Socket clientSocket = listenSocket.accept();
            processConnection(clientSocket);
        }
    }

    private void processConnection(Socket clientSocket) throws IOException {
        String log = String.format("[Ip:%s,Port:%d] 客户端上线",clientSocket.getInetAddress(),
                                                            clientSocket.getPort());
        System.out.println(log);
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()){
            while(true){
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNextLine()){
                    log = String.format("[Ip:%s,Port:%d] 客户端下线",clientSocket.getInetAddress(),
                            clientSocket.getPort());
                    System.out.println(log);
                    break;
                }
                String request = scanner.next();
                String response = process(request);
                PrintWriter writer = new PrintWriter(outputStream);
                writer.println(response);
                writer.flush();
                log = String.format("[%s,%d] 请求: %s ; 响应: %s",clientSocket.getInetAddress().toString(),
                                    clientSocket.getPort(),request,response);
                System.out.println(log);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            clientSocket.close();
        }
    }

    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        Server server = new Server(9090);
        server.start();
    }
}

客户端

public class Client {
    private Socket socket = null;
    private String serverIp;
    private int serverPort;

    public Client(String serverIp, int serverPort) throws IOException {
        this.socket = new Socket(serverIp,serverPort);
        this.serverIp = serverIp;
        this.serverPort = serverPort;
    }
    public void start(){
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()){
            while(true){
                System.out.println("->");
                String request = scanner.next();
                if(request.equals("exit")){
                    System.out.println("退出客户端");
                    break;
                }
                PrintWriter writer = new PrintWriter(outputStream);
                writer.println(request);
                writer.flush();
                Scanner responseScanner = new Scanner(inputStream);
                String response = responseScanner.next();
                String log = String.format("[请求 %s : 响应 : %s]",request,response);
                System.out.println(log);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        Client client = new Client("127.0.0.1",9090);
        client.start();
    }
}

具体分析

5.2 服务器优化

当有客户端连接时,accetp返回一个Socket对象,得到clinetSocket,并进入processConnection方法,在这个方法中,代码会在Scanner.hasNext()位置阻塞,等待客户端发送请求,客户端发送请求后,Scanner.hasNext()结束阻塞,读取到数据后,进行下面计算响应,返回响应的操作,这一系列操作完成后,又会到Scanner.hasNext()这里阻塞,直到这个客户端退出连接,循环才结束

所以此时,这个服务器同一时间只能与一个客户端建立连接,当有多个客户端同时想要与服务器建立连接时,只能与先请求建立连接的那个客户端建立连接,等到第一个客户端推出连接后,第二个客户端才能与服务器建立连接,发送请求。

解决方案:使用多线程

主线程里面循环调用accept,每次获取到一个连接,就创建一个线程,让这个子线程来处理这个连接

public class TcpThreadEchoServer {
    private ServerSocket listenSocket = null;
    public TcpThreadEchoServer(int port) throws IOException {
        listenSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            //采用多线程的方式,能够保证accept调用完毕之后能后再次立刻调用accept 每个线程对应一个客户端
            //主线程循环调用accept,客户端连接后创建一个新线程来处理连接,
            Socket clientScoket = listenSocket.accept();
            //创建线程来给客户端提供服务
            Thread t = new Thread(){
                @Override
                public void run() {
                    try {
                        processConnection(clientScoket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            };
            t.start();
        }
    }

    private void processConnection(Socket clientScoket) throws IOException {
        String log = String.format("[%s,%d]客户端上线",clientScoket.getInetAddress().toString(),
                                                        clientScoket.getPort());
        System.out.println(log);
        try (InputStream inputStream = clientScoket.getInputStream();
             OutputStream outputStream = clientScoket.getOutputStream()){
            while (true){
                //1.读取请求并解析
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNextLine()){
                    log = String.format("[%s,%d]客户端下线",clientScoket.getInetAddress().toString(),
                            clientScoket.getPort());
                    System.out.println(log);
                    break;
                }
                String request = scanner.next();
                //2.根据请求计算响应
                String response = process(request);
                //3.将响应返回给客户端
                PrintWriter writer = new PrintWriter(outputStream);
                writer.println(response);
                writer.flush();
                log = String.format("[%s:%d] res %s ; resp %s",
                        clientScoket.getInetAddress().toString(),
                        clientScoket.getPort(),request,response);
                System.out.println(log);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            clientScoket.close();
        }
    }

    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpThreadEchoServer server = new TcpThreadEchoServer(9090);
        server.start();
    }
}

总体来看,代码和之前的回显服务器的代码差别不大,只是在处理连接时,加入了多线程。

但是如果有很多的客户端连接又退出,就会导致服务器频繁的创建和销毁线程,这时就需要很大的成本,所以我们直接引入线程池,

public class TcpThreadPoolServer {
    private ServerSocket listenSocket = null;
    public TcpThreadPoolServer(int port) throws IOException {
        this.listenSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动");
        //创建线程池,将处理连接这个任务循环放入到线程池中
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true){
            Socket clientSocket = listenSocket.accept();
            //使用线程池处理连接
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
    //其他代码一样,只需要在start方法中加入线程池

5.3 英译汉服务

基于上面改进后的TCP服务器,实现一个翻译服务器。

写了几次的服务器后,我们发现翻译服务器的代码和回显服务器的代码基本差不多,只有process不一样,所以我们可以通过继承的方式来实现代码复用,只需要重写process方法,重新实现 根据请求计算响应的逻辑 就可以了

public class TcpDictServer extends TcpThreadPoolServer{
    private HashMap<String,String> map = new HashMap<>();
    public TcpDictServer(int port) throws IOException {
        super(port);
        map.put("hello","你好");
        map.put("cat","猫");
        map.put("dog","狗");

    }
    @Override
    public String process(String request){
        return map.getOrDefault(request,"我不会");
    }

    public static void main(String[] args) throws IOException {
        TcpDictServer server = new TcpDictServer(9090);
        server.start();
    }
}

你可能感兴趣的:(JavaEE,java)