网络编程 — socket套接字 — 网络编程

目录

一、什么是Socket套接字

二、UDP数据包套接字编程

1.DatagramSocket API

(1)关于Socket对象

(2)DatagramSocket方法

2. DatagramPacket API

DatagramPacket方法

3.基于UDP Socket的客户端服务器程序(回显服务器echo server)

4.单词翻译服务器

三、TCP数据包套接字编程

1.SeverSocket API

2.Socket API

3.基于TCP套接字的回显程序

一、什么是Socket套接字

数据报Datagram,通过网络传输的数据的基本单元,包含一个报头(header)和数据本身,其中报头描述了数据的目的地以及和其它数据之间的关系。

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

TCP/IP五层网络模型:应用层,传输层网络层,数据链路层,物理层。其中应用层主要是应用程序,传输层和网络层是是由系统内核封装的,数据链路层和物理层主要是由硬件和驱动实现的。在网络分层下,数据的传输离不开封装和分用。

程序员写网络程序 ,主要编写的是应用层代码,其他下面四层是程序员无法改变的。当应用程序需要将数据上传,此时就需要上层协议,调用下层协议,应用层调用传输层,传输层给应用层提供一组api,这组api就是套接字socket

系统主要给我们提供两组Socket api

        1.基于UDP的api ;

        2.基于TCP的api。

UDP协议和TCP协议的特点

UDP:无连接,不可靠传输,面向数据报,全双工

TCP:有连接,可靠传输,面向字节流,全双工

有/无连接:使用UDP/TCP通信的双方,各自是否需要刻意保存对端的相关信息;

可靠传输:信息发出去,尽可能的传输过去。

不可靠传输:信息发出去,不关注结果,不关注是否传输过去。

面向数据报:以一个UDP数据报为传输的基本单位。

面向字节流:以字节流为传输的基本单位,读写方式十分灵活。

全双工/半双工:一条路径双/单向通信。

二、UDP数据包套接字编程

主要提供了两个类:DatagramSocket(Socket对象),DatagramPacket(udp数据报)

关于“报”,是网络传输数据的基本单位,这些基本单位主要包括:报(datagram)(udp中使用),包(packet)(ip中使用),段(segment)(tcp中使用),帧(frame)(数据链路层中使用)。日常生活中,不会特意区分这些单位,但是写研究论文需要区分。

1.DatagramSocket API

(1)关于Socket对象

Socket对象:相当于对应到系统中的一个特殊文件(socket文件),这个文件并非对应到硬盘上的某个数据存储区域,而是对应到网卡这个硬件设备。进行网络通信,离不开socket文件这样的对象,借助socket文件对象,才能间接的操作网卡(相当于遥控器)。

向socket对象中数据,相当于通过网卡发送消息;向socket对象中数据,就相当于通过网卡接收消息。

下图片的以太网适配器就是一个有线网卡:

网络编程 — socket套接字 — 网络编程_第1张图片

 无线网卡:

网络编程 — socket套接字 — 网络编程_第2张图片

没有网卡就不能上网,一般是集成在主板上的。

文件广义上,代指很多计算机中的软件/硬件资源;狭义上,代指硬盘上的一块数据存储区域。

DatagramSocket是UDP Socket用于发送和接收UDP数据报。

(2)DatagramSocket方法

DatagramSocket构造方法:绑定一个端口号(服务器),也可以不显示指定客户端

方法签名

方法说明

DatagramSocket

创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口 (一般用于客户端)

DatagramSocket(int port)

创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端)

DatagramSocket(int port):此处Socket对象可能被客户端/服务器使用,服务器的socket往往需要关联一个具体的端口号(不变);客户端这里不需要手动指定,系统自动分配即可(可以改变)。

DatagramSocket方法:

方法签名

方法说明

void receive(DatagramPacket p)

从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)

void send(DatagramPacket p)

从此套接字发送数据报包(不会阻塞等待,直接发送)

void close()

关闭此数据报套接字,与文件操作一样,用完一定要关闭,否则会出现文件泄露问题

2. DatagramPacket API

DatagramPacket是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()

获取数据报中的数据

3.基于UDP Socket的客户端服务器程序(回显服务器echo server)

作用:客户端发送一个请求,服务器返回一个一模一样的响应。没有什么实际作用,只保留了最核心的发送接收环节。

服务器的三个核心工作:

  • 读取请求并解析

  • 根据请求计算响应

  • 把相应返回到客户端

整个回显服务器执行流程:

  1. 客户端读取用户输入

  2. 客户端构造请求,并发送

  3. 服务器读取用户请求数据

  4. 服务器根据请求计算响应

  5. 服务器把响应写回到客户端

  6. 客户端读取服务器的响应

  7. 客户端把响应转成字符串,并显示出来

以下代码执行顺序:

  1. 服务器先启动,执行到receive进行阻塞

  2. 客户端运行之后,从控制台读取数据,并进行send

  3. 此时客户端和服务器同时进行。客户端这边,send以后,继续往下执行,在receive里读取响应,读取以后会阻塞等待;服务器这边,就从receive返回,读取到请求数据(从客户端来的),往下走到process生成响应,然后再往下走到send并打印日志

  4. 客户端这边,当收到send回到的数据以后,就会接触阻塞,进行打印操作;服务器这边进入下一顿循环,再次阻塞到receive这里

  5. 客户端继续进行下一轮循环,阻塞在scanner.next这里等待用户执行新的内容

具体代码如下:

(1)服务器程序

package network;

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

public class UdpEchoServer {
    //定义一个socket对象
    //通过网络通信,必须要使用socket对象
    private DatagramSocket socket = null;//定义一个socket对象
    //绑定一个端口,不一定能成功
    //某个端口已经被别的进程占用了,此时绑定操作就会出错
    //同一个主机上,一个端口,同一时刻,只能被一个进程绑定
    public UdpEchoServer(int port) throws SocketException {
        //构造socket时,指定要关联/绑定的端口
        socket = new DatagramSocket(port);
    }

    //启动服务器的侏罗纪
    public void start() throws IOException {
        System.out.println("服务器启动");
        while(true) {//时刻向客户端提供服务
            //每次循环,有三个步骤:
            //1.读取请求并解析
            //构造空对象
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
           //从网卡中读取数据
            //一旦服务器start,就会立即执行receive,如果此时客户端没有发送数据,此时的receive就是阻塞状态
            socket.receive(requestPacket);
            //将数据包(二进制)转换成string,处理此请求
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            //2.根据请求计算响应(省略)
            String response = process(request);
            //3.把响应结果写回到客户端
            //根据response字符串。构造一个DatagramPacket
            //与请求packet不同,此处构造
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length
                //requestPacket是从客户端收来的,getSocketAddress就会得到客户端的ip和端口
                    ,requestPacket.getSocketAddress());
            //客户端send请求,服务器receive请求
            //服务器send响应,客户端receive响应
            socket.send(responsePacket);
            System.out.printf("[%s:%D]req:%s,resp:%s\n",responsePacket.getAddress().toString(),
                    requestPacket.getPort(),request,response);

        }
    }

    //此方法作用是根据请求计算响应
    //回显程序中没有写具体的请求和回应
    //如果写别的程序就可以修改process方法,根据需求重新构造响应
    public String process(String request) {
        return request;
    }

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

    }
}

(2)客户端程序

package network;

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;
    public UdpEchoClient(String serverIP,int serverPort) throws SocketException {
        //对于客户端来说,不需要关联端口
        //不代表没有端口,只是系统分配了一个吻空闲的端口
        socket = new DatagramSocket();
        this.serverIP = serverIP;
        this.serverPort = serverPort;
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            //1.先从控制台读取一个字符串
            //先打印一个提示符,提示用户要输入内容
            System.out.println("->");
            String request = scanner.next();
            //2.把字符串构造成UDP packet,并进行发送
            //这个构造,就是把数据构造成DatagramPacket,一方面需要String中的getBytes数组,另一方面,需要指定服务器的ip和端口
            //此处不是通过inetAddress直接构造的,而是分开设置的,一方面设置字符串的ip,一方面设置端口号
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(serverIP),serverPort);
            socket.send(requestPacket);
            //3.客户端尝试读取服务器返回的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);
            //4.把响应数据转换成String显示出来
            String response = new String(responsePacket.getData(),0,requestPacket.getLength());
            System.out.printf("%req:%s,resp:%s\n",request,response);
        }
    }

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

  对于UdpEchoServer来说,socket对象出循环就结束了生命周期,循环结束,意味着start结束,意味着main方法结束,即进程结束,当进程技结束以后,所有的文件资源就自动释放了,所以上述代码就不必显示调用close方法。 

4.单词翻译服务器

package network;

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

//使用继承,复用前面写过的代码
public class UdpDictServer extends UdpEchoServer{
    //使用一个hash表,存储单词
    //翻译的本质就是查表
    private Map dict = new HashMap<>();

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

        //向表中添加元素
        dict.put("dog","小狗");
        dict.put("cat","小猫");
        dict.put("fuck","卧槽");

    }

    //根据请求计算响应

    @Override
    public String process(String request) {
        return dict.getOrDefault(request,"单词没有查到");
    }

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

网络编程 — socket套接字 — 网络编程_第3张图片

三、TCP数据包套接字编程

1.SeverSocket API

ServerSocket构造方法:构造是指定一个具体端口,让服务器绑定该端口。

方法签名

方法说明

ServerSocket(int port)

创建一个服务端流套接字Socket,并绑定到指定端口

ServerSocket方法:

accept()方法就是接受,服务器是被动接受的一方,客户端是主动接受的一方

方法签名

方法说明

Socket accept()

开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待

Void close()

关闭此套接字

2.Socket API

Socket 构造方法:服务器IP和端口号,在客户端new Socket对象的时候,就会尝试和指定的ip端口的目标建立连接

方法签名

方法说明

Socket(String host, intport)

创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接

Socket方法:面向字节流,通过Socket对象拿到字节流对象,就可以通过字节流对象进行数据传输了,从InputStream里面读取数据,就相当于从网卡接收;在OutPutStream里面写数据,就相当于从网卡发送。

方法签名

方法说明

InetAddress getInetAddress()

返回套接字所连接的地址

InputStream getInputStream()

返回此套接字的输入流

OutputStream getOutputStream()

返回此套接字的输出流

3.基于TCP套接字的回显程序

长连接(long connnection),指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包。

短连接(short connnection),是相对于长连接而言的概念,指的是在数据传送过程中,只在需要发送数据时才去建立一个连接,数据发送完成后则断开此连接,即每次连接只完成一项业务的发送。

基于TCP套接字的回显程序执行顺序:结合代码观看

1.TcpEchoServer线启动,运行start(),阻塞状态

2.TcpEchoClient启动,会调用socket的构造方法,和服务器进行连接,连接成功之后,accept就会返回

3.对于服务器,进入processConnection方法,尝试从客户端读取请求,由于此时客户端没有发送请求,此时读取操作处于阻塞状态;对于客户端,从控制台读取用户输入。

4.当用户输入后,客户端就会真正发送请求,同时往下执行到,读取服务器响应,再次阻塞。

5.服务器收到客户端的请求之后,从next返回,执行process,执行println将响应写回给客户端。

6.服务器重新回到循环开头位置,继续尝试读取请求,并且阻塞;客户端收到服务器的响应,就可以把结果显示出来了,同时进行下次循环,等待用户输入。

(1)TcpEchoServer

package network;

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 TcpEchoServer {
    //serverSocket相当于外场的拉客中介,只能有一个
    private ServerSocket serverSocket = null;

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

    public void start() throws IOException {
        //使用线程池,使得一个连接的所有请求处理完,该线程不会直接被销毁,而是换到池子里面,下次连接时还能直接使用
        ExecutorService executorService = Executors.newCachedThreadPool();
        System.out.println("服务器启动");
        while (true) {
            //clientSocket相当于内场带每个用户讲解房子的中介,可以有多个
            Socket clientSocket = serverSocket.accept();
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
    //通过processConnection此方法来处理一个连接
    //读取请求
    //根据请求计算响应
    //将相应返回到客户端
    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d]客户端上线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
       //try()中,允许写多个流对象,多个对象中使用分号来分割
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
            //相当于字符流
            //使用scanner的方式读取数据
            Scanner scanner = new Scanner(inputStream);
            //将outputStream转换为字符流
            PrintWriter printWriter = new PrintWriter(outputStream);
            while(true) {
                //1.读取请求
                //每个请求是个字符串(文本数据)
                //请求与请求之间,使用\n来分割
                //hasNext():判断接下来是否还有数据
                //如果客户端关闭连接,hasNext就会返回为false,循环就会结束
                if (!scanner.hasNext()) {
                    //读取到流结尾(也就是客户端关闭了)
                    //输出客户端ip地址,输出客户端端口号
                    System.out.printf("[%s:%d]客户端下线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                //如果客户端有数据,hasNext就会返回为true,进一步使用下面的next方法来读取出这一段字符串的内容
                //直接使用scanner读取一段字符串
                //next往后一直都,督导空白符结束(空格,换行,制表符,翻页符等等)
                String request = scanner.next();
                //2.根据请求计算响应
                String response = process(request);
                //3.把响应写回给客户端,next读操作结果是不带换行的,响应也需要带上换行
                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 {
            //clientSocket可以有多个,而且生命周期端,所以需要关闭
            clientSocket.close();
        }
    }
    //根据请求计算响应
    private String process(String request) {
         return request;
    }

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

(2)TcpEchoClient

package network;

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


public class TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient(String serveIp,int port) throws IOException {
        //将客户端于服务端建立TCP连接
        //连接上了,服务器的accept就会返回,在此之前,accept是处于阻塞状态的
        socket = new Socket(serveIp,port);
    }

    public void start() {
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            PrintWriter printWriter = new PrintWriter(outputStream);
            Scanner scannerFromSocket = new Scanner(inputStream);
            while (true) {
                //1.把键盘上读取用户输入的内容
                System.out.print("->");
                String request = scanner.next();
                //2.把读取的内容构造成请求,发送给服务器
                //发送带有换行
                //以下步骤只是把数据写入了内存的缓冲区中,等到缓冲区满了(没有满是因为数据不够多),才会真正写网卡
                printWriter.println(request);
                //使用flush()方法,手动刷新缓冲区,将数据立即写入网卡
                printWriter.flush();
                //3.从服务器读取相应内容
                String response = scannerFromSocket.next();
                //4.把响应结果显示到控制台上
                System.out.printf("req:%s;resp:%s\n", request, response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

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