网络编程套接字

网络编程套接字

研究网络编程套接字就是研究如何写代码完成网络编程

socket套接字是操作系统给应用程序提供的API

应用层和传输层是可以进行交互的,socket就是传输层给应用层提供的

在网络编程中, 有很多 的协议 ,最知名的协议就是TCP和UDP协议

这两种协议的工作特性差别比较大, 因此操作系统提供了两个版本的API

TCP和UDP的区别

TCP

  • 有连接
  • 可靠传输
  • 面向字节流
  • 全双工

UDP

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

所谓的有连接就像是打电话, 只有对方接了电话,才能进行通信

只有双方建立好连接才能进行交互数据

可靠传输并不是说A 给B发消息,消息就100%会发送过去,因为网络环境十分的复杂,没办法保证100%能送到

可靠传输的意思是A至少能知道B有没有收到消息

TCP是面向字节流, 和文件操作是一样的,都是流的形式

UDP是面向数据报,基本单位是数据报

全双工相对的词是半双工

全双工 : 一个通道,双向通信

半双工 : 一个通道, 单向通信

UDP的套接字

UDP中需要掌握的类

  1. DatagramSocket
  2. DatagramPacket

image-20221030141947193

文件操作: 先打开文件 然后读/写文件, 最后关闭文件

事实上,socket本质上也是一种文件

狭义的文件是存储在磁盘上的文件

广义的文件 : 操作系统把各种硬件设备和软件资源都抽象成了文件, 统一按照文件的方式进行管理

socket对应到网卡这个硬件设备,操作系统也是把网卡当做文件来管理的

通过网卡发送数据就是写文件

通过网卡接收数据就是读文件

所以DatagramSocket就是网卡的代言人

DatagramPacket代表的是一个UDP的数据报,也就是一次发送和接收的基本单位

image-20221030142739017

接下来写一个UDP版本的客户端 服务器 程序的:回显服务器(客户端发什么,服务器就返回什么)

一般正经的服务器有一个很重要的环节 : 根据请求,计算出响应

但是这个回显服务器,主要是用来练练手的

UDP的服务器代码详解

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

public class UdpEchoSever {
     private DatagramSocket socket = null;

     //写一下构造方法
    //这里形参是端口号,通常情况下,一个端口号代表一个进程,所以要想通过端口号找到进程,就要先绑定端口号
    //参数的端口表示服务器要绑定的端口
    public UdpEchoSever(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }
    //使用start启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动!");
        //由于不知道客户端什么时候发送请求,所以服务器要一直工作,所以使用死循环
        while(true){
            //目标: 在循环里处理一次请求
            //1. 读取请求并解析--使用socket的receive方法

            //构造DatagramPacket空对象的时候,也是要求要分配内存空间的
            //也就是说,客户端发来请求,我先创建requestPacket,之后再装进requestPacket
            DatagramPacket requestPacket = new DatagramPacket(new byte[2048],2048);
            socket.receive(requestPacket);//这里的参数要求是DatagramPacket类型的,所以要先构造一个DatagramPacket的空的对象
            //注意: receive的参数是输出型参数,也就是说传入一个空的对象,之后要将从网卡中读到的对象重新传入到这个对象中

            //将请求转换成字符串,方便打印,new String可以传入一个字节数组,使之变成一个字符串
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

            //2. 根据请求计算响应
            String response = Response(request);

            //3. 把响应写回到客户端--使用socket的send方法
            //下面这一步其实与上面的requestPacket的构造是一样的,只是这里是具体的字节数组和长度,但是由于我要发送给客户端,所以我要知道客户端的地址,
            //客户端的地址其实就包含在requestPacket中,所以还要加上 requestPacket.getSocketAddress(),之后再进行发送
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length(),
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //4.打印日志,记录当前的情况
            //先打印出客户端的IP地址和客户端的端口号,最后是输入和输出的内容
            System.out.printf("[%s %d] request: %s  response: %s\n",requestPacket.getAddress().toString(),
                    requestPacket.getPort(),request,response);

        }
    }

    //由于是回显服务器,所以传入什么就返回什么就行了
    public  String Response(String request) {
        return request;
    }

    //创建main方法
    public static void main(String[] args) throws IOException {
        UdpEchoSever sever = new UdpEchoSever(9090);//调用构造方法
        sever.start();
    }
}

UDP的客户端代码详解

package UDP;

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

public class UdpEchoClient {
    private DatagramSocket socket = null;

    private String severIp;
    private int severPort;

    //构造方法是不需要写返回值的
    public  UdpEchoClient(String  severIp, int severPort) throws SocketException {
        //在服务器这里是要求指定一个端口号的
        //但是, 在客户端这里并不是没有端口号,而是由系统自动分配一个空闲的端口号
        //要是我们指定一个端口,万一用户此时的端口已经被占用了呢?所以在客户端上,不指定端口号,由系统自动分配
        socket = new DatagramSocket();
        //假设这里的IP地址是以1.2.3.4这样的点分十进制格式
        this.severIp = severIp;
        this.severPort = severPort;
    }
    public void start() throws IOException {
        Scanner sc = new Scanner(System.in);
        while(true){
            //1.从控制台输入请求
            System.out.println("-->");
            String request = sc.nextLine();
            //2.构造一个UDP请求,发送给服务器
            //getBytes()是将字符串转换成字节数组,后面之所以不用request.length(),是因为request.length()单位是字符
            //request.getBytes().length的单位是字节.
            //既然要发送就要说明服务器的位置,所以后面还有加上IP和port
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(this.severIp),this.severPort);
            socket.send(requestPacket);
            //3.从服务器中读取UDP响应数据,并解析
            //先创建一个新的空的responsePacket,之后在将读到的数据写进去
            DatagramPacket responsePacket = new DatagramPacket(new byte[2048],2048);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());
            //4.把服务器的响应显示到控制台上
            System.out.println(response);
        }
    }

    public static void main(String[] args) throws IOException {
        UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);//这里是目标IP和目标端口号
        client.start();
    }
}

最后的运行结果 :

image-20221030204719651

简洁版

​ 服务器

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

public class UdpEchoSever {
     private DatagramSocket socket = null;
    //端口参数表示服务器要绑定的端口
    public UdpEchoSever(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true){
            //1. 读取请求并解析--使用socket的receive方法
            DatagramPacket requestPacket = new DatagramPacket(new byte[2048],2048);
            socket.receive(requestPacket);

            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

            //2. 根据请求计算响应
            String response = Response(request);

            //3. 把响应写回到客户端--使用socket的send方法
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //4.打印日志,记录当前的情况
            System.out.printf("[%s %d] request: %s  response: %s\n",requestPacket.getAddress().toString(),
                    requestPacket.getPort(),request,response);
        }
    }
    public  String Response(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        UdpEchoSever sever = new UdpEchoSever(9090);//调用构造方法
        sever.start();
    }
}

客户端:

package UDP;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String severIp;
    private int severPort;

    public  UdpEchoClient(String  severIp, int severPort) throws SocketException {
        socket = new DatagramSocket();//自动分配端口号
        this.severIp = severIp;
        this.severPort = severPort;
    }
    public void start() throws IOException {
        Scanner sc = new Scanner(System.in);
        while(true){
            //1.从控制台输入请求
            System.out.println("-->");
            String request = sc.nextLine();//遇到空格不会停止
            //2.构造一个UDP请求,发送给服务器--使用socket.send()
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(this.severIp),this.severPort);
            socket.send(requestPacket);
            //3.从服务器中读取UDP响应数据,并解析--使用socket.receive()
            DatagramPacket responsePacket = new DatagramPacket(new byte[2048],2048);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());
            //4.把服务器的响应显示到控制台上
            System.out.println(response);
        }
    }
    public static void main(String[] args) throws IOException {
        UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);//这里是目标IP和目标端口号
        client.start();
    }
}

服务器的构造方法 : 只有一个port

image-20221030191003480

客户端的构造方法: IP地址和端口号都要输入

image-20221030190913898

主要的原因就是服务器的IP地址就是本机的IP地址,一般来说, 是没有必要输入的

但是,客户端有必要输入一下是哪一台服务器的IP地址和端口号,也就是目标IP地址和目标端口号

端口号

端口号是一个十六位的整数, 范围是0-65535,但是一般都是是使用1024-65535,因为0-1023都是已经被一些知名的应用程序占用了

客户端和服务器的调用过程:

image-20221030200816144

对于服务器来说 ,这三个步骤: 读取请求并解析, 根据请求计算响应, 把响应写回客户端,这些步骤极快,所以即使有多个客户端发来请求,服务器也是可以响应的,本质上还是串行处理

要是计算的速度比较慢就要使用到多线程了,还是不行只能使用分布式了

首先启动服务器,使用就 jconsole 就能发现在服务器程序的第30行阻塞了,也就是socket.receive()处阻塞了

image-20221030202223460

一台服务器要能够给多个客户端提供服务,如何在IDEA中启动多个客户端来运行程序呢?

image-20221030205714624

image-20221030205722137

image-20221030205727901

这样子就能创建多个客户端了

image-20221030211007382

以上的回显服务器只是练练手的,并没有什么实际意义

下面写一个查询单词服务器

继承一下UdpEchoServer,只要修改一下根据请求计算响应的具体内容就行了

客户端是不用动的,是通用的

package UDP;//一定要把包导进去
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
//写一个翻译的服务器-客户端
//直接使用继承,start一样的,只要修改一下计算响应的方法就行了
//所以的翻译就是使用哈希表key->value 对应的关系
public class UdpTranslateServer extends UdpEchoServer{
    HashMap<String, String> dict = new HashMap<>();
    public UdpTranslateServer(int port) throws SocketException {
        super(port);
        dict.put("falsh","闪电");
        dict.put("dog", "狗");
        dict.put("cat","猫");
    }

    @Override
    public String Response(String request) {
        //输入key,要是在哈希表中有这个key,就返回对应的value,要是没有,就返回后面的话
        return dict.getOrDefault(request,"查不到这个单词");
    }

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

一个服务器程序的基本流程是和上面一样的,但是最核心的就是"根据请求计算响应" => 服务器的业务逻辑

TCP 的套接字

TCP主要也是要回两个类

ServerSocket

Socket

下面是具体的类

SeverSocket类 :

image-20221031191122615

Socket类: image-20221031191545949

服务器程序

package TCP;
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 {
    private  ServerSocket listenSocket = null;
    private TcpEchoServer(int port) throws IOException {
        listenSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动");
        //
        while(true){
            //1.调用accept接收客户端的连接
            //listenSocket就像是拉客的,拉完之后交给clientSocket来进行一对一处理
            Socket clientSocket = listenSocket.accept();
            //2.处理这个连接
            processConnection(clientSocket);
        }
    }

    private static void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s %d] 客户端上线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //处理客户端请求
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
            while(true){
                //1.读取请求并响应
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNext()){
                    //读完了,可以断开客户端的连接了
                    System.out.printf("[%s %d] 客户端下线",clientSocket.getInetAddress().toString(),
                            clientSocket.getPort());
                    break;
                }
                String request = scanner.next();
                //2.根据请求计算响应
                String response = process(request);
                //3.将响应写回客户端
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                //刷新缓冲区,确保服务器真的发回给客户端了
                printWriter.flush();
                System.out.printf("[%s %d] req: %s res: %s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
                    request,response);

            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //为什么只有clientSocket最后要被释放?前面的listenSocket不用手动释放?
            //一个进程中能打开的文件是有限的,也就是说文件描述符表是有限的
            //listenSocket在TCP服务器中是唯一的对象,不会占满文件描述符表,进程退出,会自动销毁
            //clientSocket在循环中,每一个客户端都要分配一个,会被反复创建实例,每创建一个就要消耗一个文件描述符表,所以要及时销毁

            clientSocket.close();//最后要将clientSocket销毁掉
        }

    }

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

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

客户端程序

package TCP;

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

public class TcpEchoClient {
    //客户端通过Socket建立连接
   private Socket socket = null;
   public  TcpEchoClient(String serverIP, int serverPort) throws IOException {
       //要想建立连接,就要知道服务器的地址
       //由于TCP是有连接,所以要先连接上,所以new的时候要加上形参
       socket  = new Socket(serverIP,serverPort);
   }
   public void start(){
       Scanner scanner = new Scanner(System.in);
       try(InputStream inputStream = socket.getInputStream();
           OutputStream outputStream = socket.getOutputStream()){
           while(true){
               //1.从控制台读取数据, 构造成一个请求
               System.out.println("->");
               String request = scanner.next();
               //2.将请求发送给客户端
               PrintWriter printWriter = new PrintWriter(outputStream);
               printWriter.println(request);//发送给客户端
               printWriter.flush();//刷新缓冲区
               //3.从客户端上读取响应
               Scanner respScanner = new Scanner(inputStream);
               String response = respScanner.next();
               //4.将响应显示在显示器上
               System.out.println(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();
    }
}

上述服务器-客户端代码运行顺利

但是,一旦多开几个客户端代码,就会发现后面新的客户端不能使用,也就是说accept方法被第一个客户端占用着,后面的用不上

所以需要多线程

为什么之前写UDP代码的时候,多开几个客户端没事?

这是因为UDP直接发送消息即可.TCP建立连接之后,要处理客户端的多个请求,才导致无法快速调用accept

要是TCP每次只处理一个客户端的请求,也能保证快速调用accept

所以只要分多线程处理连接就行了

public void start() throws IOException {
        System.out.println("服务器启动");
        while(true){
            //1.调用accept接收客户端的连接
            //listenSocket就像是拉客的,拉完之后交给clientSocket来进行一对一处理
            Socket clientSocket = listenSocket.accept();
            //2.处理这个连接
            Thread thread =  new Thread(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
               thread.start();
        }
    }

但是使用多线程会频繁的创建和销毁线程,这里就可以使用线程池

public void start() throws IOException {
        System.out.println("服务器启动");
        while(true){
            //1.调用accept接收客户端的连接
            //listenSocket就像是拉客的,拉完之后交给clientSocket来进行一对一处理
            Socket clientSocket = listenSocket.accept();
            //2.处理这个连接
            ExecutorService service = Executors.newCachedThreadPool();//创建线程池
            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
    }

socket api是一切网络编程的基础,很多的框架/库/组件的底层都是基于socket, 所以这是很重要的!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值