网络编程(TCP/UDP)套接字

目录

  • 一、套接字
  • 二、UDP数据报套接字编程
    • DatagramSocket API
    • DatagramPacket API
    • InetSocketAddress API
    • 示例1:写一个简单的客户端服务程序,回显服务(`EchoSever`)
      • 五元组
      • 服务器
      • 客户端
    • 写一个翻译程序,请求是一些简单的英文单词,响应是英文单词对应的翻译:
  • 三、TCP流套接字编程
    • ServerSocket API
    • Socket API
    • TCP服务器
    • TCP客户端
      • TCP版本查字典的服务器



一、套接字

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

socket可以视为是应用层和传输层之间的通信桥梁
传输层的核心协议有两种:TCP,UDP;socket API也有对应的两组,由于TCP和UDP协议差别很大,因此,这两组API差别也挺大。

分类
Socket套接字主要针对传输层协议划分为如下三类:

  1. 流套接字:使用传输层TCP协议
    TCP,即Transmission Control Protocol(传输控制协议),传输层协议;

TCP的特点

  • 有连接:像打电话,得先接通,才能交互数据;
  • 可靠传输:传输过程中,发送方知道接收方有没有收到数据.(打电话就是可靠传输);
  • 面向字节流:以字节为单位进行传输.(非常类似于文件操作中的字节流);
  • 全双工:一条链路,双向通信;
  • 有接收缓冲区,也有发送缓冲区。
  • 大小不限

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

  1. 数据报套接字:使用传输层UDP协议
    UDP,即User Datagram Protocol(用户数据报协议),传输层协议。

UDP的特点

  • 无连接:像发微信,不需要接通,直接就能发数据;
  • 不可靠传输:传输过程中,发送方不知道接收方有没有收到数据.(发微信就是不可靠传输);
  • 面向数据报:以数据报为单位进行传输(一个数据报都会明确大小)一次发送/接收必须是一个完整的数据报,不能是半个,也不能是一个半;
  • 全双工:一条链路,双向通信;
  • 有接收缓冲区,无发送缓冲区;
  • 大小受限:一次最多传输64k;

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

  1. 原始套接字

原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。

二、UDP数据报套接字编程

UDPSocket中,主要涉及到两类:DatagramSocket、DatagramPacket;

DatagramSocket API

DatagramSocket 创建了一个UDP版本的Socket对象,用于发送和接收UDP数据报,代表着操作系统中的一个socket文件,(操作系统实现的功能–>)代表着网卡硬件设备的抽象体现。

DatagramSocket 构造方法:

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

DatagramSocket 方法

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

DatagramPacket API

代表了一个UDP数据报,是UDP Socket发送和接收的数据报,每次发送/接收数据报,都是在传输一个DatagramPacket对象。

DatagramPacket 构造方法

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

DatagramPacket 方法

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

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

InetSocketAddress API

InetSocketAddressSocketAddress 的子类 )构造方法:

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

示例1:写一个简单的客户端服务程序,回显服务(EchoSever

网络编程(TCP/UDP)套接字_第1张图片
构建Socket对象有很多失败的可能

  1. 端口号已经被占用,同一个主机的两个程序不能有相同的端口号(这就好比两个人不能拥有相同的电话号码);
    此处,多个进程不能绑定同一个端口号,但是一个进程可以绑定多个端口,(这就好比一个人可以拥有多个手机号),一个进程可以创建多个Socket对象,每个Socket都绑定自己的端口。
  2. 每个进程能够打开的文件个数是有上限的,如果进程之间已经打开了很多文件,就可能导致此时的Socket文件不能顺利打开;

在这里插入图片描述
这个长度不一定是1024,假设这里的UDP数据最长是1024,实际的数据可能不够1024.

在这里插入图片描述
这里的参数不再是一个空的字节数组了,response是刚才根据请求计算的得到的响应,是非空的,DatagramPacket 里面的数据就是String response的数据。

response.getBytes().length:这里拿到的是字节数组的长度(字节的个数),而response.length得到的是字符的长度。

五元组

一次通信是由5个核心信息描述的:源IP、 源端口、 目的IP、 目的端口、 协议类型。

站在客户端角度

  1. 源IP:本机IP;
  2. 源端口:系统分配的端口;
  3. 目的IP:服务器的IP;
  4. 目的端口:服务器的端口;
  5. 协议类型:TCP;

站在服务器的角度

  1. 源IP:服务器程序本机的IP;
  2. 源端口:服务器绑定的端口(此处手动指定了9090);
  3. 目的IP:包含在收到的数据报中(客户端的IP);
  4. 目的端口:包含在收到的数据报中(客户端的端口);
  5. 协议类型:UDP;

服务器

package UDP;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UdpEchoServer {
    //创建一个Socket实例
    private DatagramSocket socket = null;

    //port 端口号在运行程序的时候手动指定
    public UdpEchoServer(int port) throws SocketException {
        //需要显示的绑定一个端口号
        //端口号是用来区分一个应用程序的,主机收到网卡上数据的时候,这个数据应该给哪个程序?
       socket = new DatagramSocket(port);
    }
    //启动服务器
    public void start() throws IOException {
        System.out.println("启动服务器!");
        //UDP不需要建立连接,直接接收从客户端来的数据
        while (true){
            //1.读取客户端发来的请求
            DatagramPacket datagramPacket = new DatagramPacket(new byte[1024],1024);
            socket.receive(datagramPacket);//为了接收数据,需要准备好datagramPacket对象,由receive来进行接收,这里的datagramPacket为输出型参数
            //将datagramPacket解析成String
            String request = new String(datagramPacket.getData(),0,datagramPacket.getLength(),"UTF-8");
            //2.根据请求计算响应(由于这里是回显服务,所以2省略)
            String response = process(request);
            //3.把响应写回到客户端
            //第一个参数不再是一个空的字节数,第三个参数:表示要把数据发给哪个端口+地址
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,datagramPacket.getSocketAddress());
            socket.send(responsePacket);
            System.out.printf("[%s:%d] req :%s,resp: %s\n",responsePacket.getAddress().toString(),
                    responsePacket.getPort(),request,response);
        }
    }

    //由于是回显服务,响应就和请求一样
    public String process(String request) {

        return request;
    }

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

在这里插入图片描述
这里就是系统自动给客户端分配的端口;
客户端可以有很多的,一个服务器可以给很多客户端提供服务

客户端

package UDP;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private  String severIP;
    private  int severPort;
    //客户端构造socket对象时不再手动指定端口号,自己分配一个空闲的端口号
    // port是服务器的端口,客户端启动的时候不需要给socket指定端口,客户端自己的端口是系统随机指定的
    public UdpEchoClient(String ip,int port) throws SocketException {
        socket = new DatagramSocket();
        severIP = ip;
        severPort = port;
    }
    public void start() throws IOException {
        //1.从控制台读取用户的字符串
        Scanner sc = new Scanner(System.in);
        while (true){
            System.out.println("-> ");
            String request = sc.next();
            //2.把这个用户输入的内容,构造成一个UDP请求,并发送
            //构造的请求里面包含两部分信息:1.数据的内容request字符串。 2.数据要发给谁:服务器的IP+端口
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(severIP),severPort);
            socket.send(requestPacket);
            //3.从服务器读取响应数据,并解析
            DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0,responsePacket.getLength(),"UTF-8");
            //4.把响应结果显示到控制台上
            System.out.printf("req :%s,resp: %s\n",request,response);
        }

    }

    public static void main(String[] args) throws IOException {
        //服务器和客户端在同一个机器上,这里使用的IP是一样的
        UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

网络编程(TCP/UDP)套接字_第2张图片
网络编程(TCP/UDP)套接字_第3张图片

写一个翻译程序,请求是一些简单的英文单词,响应是英文单词对应的翻译:

客户端不变,服务器代码进行调整:主要是调整process方法;
读取请求并解析,把响应写回到客户端,这两步是一样的,关键逻辑就是:根据请求处理响应。

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;

public class UdpDictServer extends UdpEchoServer {
    private HashMap<String, String> dict = new HashMap<>();

    public UdpDictServer(int port) throws SocketException {
        super(port);

        // 简单构造几个词
        dict.put("cat", "小猫");
        dict.put("dog", "小狗");
        dict.put("pig", "小猪");
    }

    @Override
    public String process(String request) {
        return dict.getOrDefault(request, "该词无法被翻译!");
    }

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

三、TCP流套接字编程

TCP API 也涉及到两个核心的类:
ServerSocket :专门给TCP服务器用;
Socket :即需要给服务器用,又需要给客户端用;
主要通过这样的类,来描述一个socket文件即可,而不需要专门的类表示"传输的包";面向字节流(以字节为单位传输的)。

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 host, int port) 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连

Socket 方法

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

TCP服务器

网络编程(TCP/UDP)套接字_第4张图片
这里之所以分成了两步,就是因为要建立连接.一个专门负责建立连接,一个专门负责数据通信

package UDP;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;


public class TcpEchoServer {
    // 操作系统原生的 API 里有一个操作叫做 listen
//    private ServerSocket listenSocket = null;
    private ServerSocket serverSocket = null;


    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true){
            // 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话)
            // accept 就是在 "接电话", 接电话的前提是, 有人给你打了, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
            // accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
            // 进一步讲, serverSocket 就干了一件事, 接电话.
            Socket clientSocket = serverSocket.accept();
            processConnection(clientSocket);
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s %d] 客户端建立连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //处理请求和响应
        try(InputStream inputStream = clientSocket.getInputStream()){
            try(OutputStream outputStream = clientSocket.getOutputStream()){
               //循环处理每个请求,分别返回响应
                Scanner scanner = new Scanner(System.in);
                while (true){
                    //1.读取请求
                    if(!scanner.hasNext()){
                        System.out.printf("[%s %d] 客户端断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                        break;
                    }
                    //此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的
                    String request = scanner.next();
                    //2.根据请求计算响应
                    String response = process(request);
                    //3.把响应返回给客户端
                    // 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(response);
                    //刷新缓冲区,如果没有这个刷新, 可能客户端就不能第一时间看到响应结果.
                    printWriter.flush();

                    System.out.printf("[%s %d] req: %s, reps %s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //此处记得关闭操作
            try {
            //这个是每个连接有一个的,数目很多,连接断开,也就不再需要了~~
			//每次都得保证处理完的连接都给进行释放.
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

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

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

TCP客户端

对于UDP的socket来说,构造方法指定的端口,表示自己绑定哪个端口;
对于TCP的ServerSocket来说构造方法指定的端口,也是表示自己绑定哪个端口;
对于TCP的Socket来说构造方法指定的端口,表示要连接的服务器的端口.

UDP没有用多线程因为:
UDP不需要处理连接,UDP只要一个循环,就可以处理所有客户端的请求.但是此处, TCP既要处理连接,又要处理一个连接中的若干次请求,就需要两个循环.里层循环,就会影响到外层循环的进度了.
主线程,循环调用accept .当有客户端连接上来的时候,就直接让主线程创建一个新线程。由新线程负责对客户端的若干个请求,提供服务.(在新线程里,通过while循环来处理请求).这个时候,多个线程是并发执行的关系(宏观上看起来同时执行),就是各自执行各自的了,就不会相互干扰(也要注意:每个客户端连上来都得分配一个线程)

多线程版本,可以同时与多个客户端进行通信:

package UDP;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TcpThreadEchoSever {
    // 操作系统原生的 API 里有一个操作叫做 listen
//    private ServerSocket listenSocket = null;
    private ServerSocket serverSocket = null;


    public TcpThreadEchoSever(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true){
            // 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话)
            // accept 就是在 "接电话", 接电话的前提是, 有人给你打了, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
            // accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
            // 进一步讲, serverSocket 就干了一件事, 接电话.
            Socket clientSocket = serverSocket.accept();
            //改进方法,每次accept成功,都创建一个新的线程,由新线程负责执行clientSocket方法
            Thread t = new Thread(()->{
                processConnection(clientSocket);
            });
            t.start();
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端建立连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //处理请求和响应
        try(InputStream inputStream = clientSocket.getInputStream()){
            try(OutputStream outputStream = clientSocket.getOutputStream()){
                //循环处理每个请求,分别返回响应
                Scanner scanner = new Scanner(System.in);
                while (true){
                    //1.读取请求
                    if(!scanner.hasNext()){
                        System.out.printf("[%s:%d] 客户端断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                        break;
                    }
                    //此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的
                    String request = scanner.next();
                    //2.根据请求计算响应
                    String response = process(request);
                    //3.把响应返回给客户端
                    // 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(response);
                    //刷新缓冲区,如果没有这个刷新, 可能客户端就不能第一时间看到响应结果.
                    printWriter.flush();

                    System.out.printf("[%s:%d] req: %s, reps %s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //此处记得关闭操作
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

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

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

TCP服务器线程池版本:

package UDP;


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpThreadPollEchoServer {
    // 但是在 Java socket 中是体现不出来 "监听" 的含义的~~
    // 之所以这么叫, 其实是 操作系统原生的 API 里有一个操作叫做 listen
    // private ServerSocket listenSocket = null;
    private ServerSocket serverSocket = null;

    public TcpThreadPollEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        //线程池
        ExecutorService pool = Executors.newCachedThreadPool();
        while (true) {
            // 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话)
            // accept 就是在 "接电话", 接电话的前提是, 有人给你打了~~, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
            // accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
            // 进一步讲, serverSocket 就干了一件事, 接电话~~
            Socket clientSocket = serverSocket.accept();
            // [改进方法] 在这个地方, 每次 accept 成功, 都创建一个新的线程, 由新线程负责执行这个 processConnection 方法~
            // 通过线程池来实现
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        // 接下来来处理请求和响应
        // 这里的针对 TCP socket 的读写就和文件读写是一模一样的!!
        try (InputStream inputStream = clientSocket.getInputStream()) {
            try (OutputStream outputStream = clientSocket.getOutputStream()) {
                // 循环的处理每个请求, 分别返回响应
                Scanner scanner = new Scanner(inputStream);
                while (true) {
                    // 1. 读取请求
                    if (!scanner.hasNext()) {
                        System.out.printf("[%s:%d] 客户端断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                        break;
                    }
                    // 此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的
                    String request = scanner.next();
                    // 2. 根据请求, 计算响应
                    String response = process(request);
                    // 3. 把这个响应返回给客户端
                    // 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(response);
                    // 刷新缓冲区, 如果没有这个刷新, 可能客户端就不能第一时间看到响应结果.
                    printWriter.flush();

                    System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(),
                            clientSocket.getPort(), request, response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 此处要记得来个关闭操作.
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

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

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

TCP版本查字典的服务器

package UDP;

import java.io.IOException;
import java.util.HashMap;


public class TcpDictSever extends TcpThreadPollEchoServer {
    private HashMap<String,String> map = new HashMap<>();
    public TcpDictSever(int port) throws IOException {
        super(port);
        
        map.put("cat","猫");
        map.put("pig","猪");
        map.put("dog","狗");
    }
    

    @Override
    public String process(String request) {
        return map.getOrDefault(request,"当前词组无法找到!");
    }

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

以上。

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