网络编程套接字

网络编程套接字

是操作系统给应用程序提供的一组API(叫做socket API)
socket可以视为是应用层与传输层的通信桥梁
传输层的核心协议有两种 TCP UDP
socket API 也有对应的两组
由于TCP UDP协议差别很大,因此,这两组API差别也挺大

TCP与UDP的区别

TCP

(1)有连接

像打电话。得先接通,才能交互数据
(2)可靠传输
传输过程中,发送方知道接收方有没有收到数据

错误理解:
1.就是数据发过去之后100%能被对方收到
2. 可靠传输就是安全传输

(3)面向字节流
以字节为单位进行传输
(4)全双工
一条链路,双向通信

半双工:一条链路,单向通信

UDP

(1)无连接
像发微信。不用接通,直接就能发数据
(2)不可靠传输
传输过程中,发送方不知道接收方有没有收到数据
(3)面向数据流
以数据报为单位进行传输(一个数据报都会明确大小),一次发送 / 接受必须是一个完整的数据报,不能是半个,也不能是一个半。
(4)全双工

UDP socket

五元组:
一次通信是由五个核心信息描述出来的:源IP ,源端口, 目的IP ,目的端口,协议类型。
站在客户端的角度:
源IP:本机IP
源端口:系统分配的端口
目的IP:服务器的IP
目的端口:服务器端口
协议类型:UDP
站在服务器的角度:
源IP:服务器程序本机的IP
源端口:服务器绑定的端口(此处手动指定了9090)
目的IP:客户端IP(包含在收到的数据报中)
目的端口:客户端的端口(包含在收到的数据报中)
协议类型:UDP

UDP socket中,主要涉及到两个类

  1. DatagramSocket
    创建了一个UDP版本的socket对象
    Datagram:数据报
    Socket对象,就代表操作系统中的一个socket文件。socket文件,代表网卡硬件设备抽象体现。
    从socket文件读数据,本质就是读网卡
    往socket文件写数据,本质就是写网卡
  2. DatagramPacket
    代表了一个UDP数据报,使用UDP传输数据的基本单位.
    每次发送/ 接收数据,都是在传输一个DatagramPacket对象

接下来写一个简单的客户端服务器程序,回显服务(EchoServer)
回显:请求的内容是什么,得到的回应就是什么
这样的程序属于最简单的网络编程中的程序,不涉及到任何的业务逻辑,就只是通过socket API单纯的转发

UDP服务器

public class UdpEchoServer {
    private DatagramSocket socket=null;
    public UdpEchoServer(int port) throws SocketException {
        socket=new DatagramSocket(port);
    }
    //启动服务器
    public void start() throws IOException {
        System.out.println("启动服务器");
        //UDP不需要建立连接,直接接收从客户端发来的数据
        while(true){
            //1.读取客户端发来的请求
            DatagramPacket requestPacket=new DatagramPacket(new byte[1024],1024);
            socket.receive(requestPacket);//为了接收数据,需要先准备一个空的DatagarmPacket对象,由receive来进行填充数据
            //把DatagramPacket解析成一个String
            String request=new String(requestPacket.getData(),0, requestPacket.getLength(),"UTF-8");
            //2.根据请求计算响应(这里是回显服务,2可以省略)
            String response=process(request);
            //3.响应写回到客户端
            DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);
            System.out.printf("[%s,%d] req: %s,resp:%s\n",requestPacket.getAddress().toString(),
                    requestPacket.getPort(),request,response);
        }
    }

    //由于是回显服务,响应就和请求一样了
    //实际上对于一个真实的服务器来说,这个过程是非常复杂的
    private String process(String request) {
        return  request;
    }

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

在这里插入图片描述

UDP客户端

public class UdpEchoClient {
    private DatagramSocket socket=null;
    private  String serverIP;
    private  int  serverPort;
    public UdpEchoClient(String ip,int port) throws SocketException {
        //此处的port是服务器的端口
        //客户端启动的时候,不需要给socket指定端口,客户端自己的端口是系统随机分配的
        socket=new DatagramSocket();
        serverIP=ip;
        serverPort=port;
    }
    public void start() throws IOException {
        Scanner scanner=new Scanner(System.in);
        while(true){
            //1.先从控制台读取用户输入的字符串
            System.out.println("-> ");
            String request=scanner.next();
            //2.把这个用户输入的内容,构造成一个UDP请求,并发送
            //   构造的请求里包含两部分信息
            //   1)数据的内容,request字符串
             //   2)数据要发给谁,服务器的 IP+端口
            DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(serverIP),serverPort);
            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 {
        UdpEchoClient client=new UdpEchoClient("127.0.0.1",9090);
        client.start();
    }

}

在这里插入图片描述

执行效果
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

翻译程序(英译汉)

接下来写一个简单的程序,带上业务逻辑。
请求是一些简单的英文单词,响应就是对应单词的翻译

客户端不变,把服务器的代码进行调整,主要是process方法。

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("monkey","小猴");
        dict.put("cow","小牛");
        dict.put("wolf","小狼");

    }


    @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服务器用的)
Socker(既需要给服务器用,又需要给客户端用)

TCP服务器

public class TcpEchoServer {
   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());
        //接下来处理请求和响应
        //这里的针对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;
                    }
                    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();
            }
        }

    }

    private String process(String request) {

       return  request;
    }

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

}

TCP客户端

public class TcpEchoClient {
    //用普通的socket即可
    //此处也不用手动给客户端指定端口号,让系统自由分配
    private Socket socket=null;
    public  TcpEchoClient(String serverIP,int serverPort) throws IOException {
        //这里的IP和端口号的含义表示的不是自己绑定,而是表示和这个IP 端口号建立连接
        //调用这个构造方法,就会和服务器建立连接(打电话拨号了)
        socket=new Socket(serverIP,serverPort);
    }

    public void start(){
        System.out.println("和服务器连接成功");
        Scanner scanner=new Scanner(System.in);
        try(InputStream inputStream= socket.getInputStream()){
            try(OutputStream outputStream= socket.getOutputStream()){
                while (true) {
                    //1.从控制台读取字符串
                    System.out.print("->");
                    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.printf("req:%s,resp:%s\n",request,response);

                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

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

     }

}

运行结果
在这里插入图片描述
在这里插入图片描述

虽然上述的TCP代码已经跑起来了,但是此处还存在一个很严重的问题,当前的服务器,同一时刻只能处理一个连接
在这里插入图片描述
要想解决上述问题,就得让processConnection和前面的accept的执行互相不干扰,不能让processConnection里面的循环导致accept无法及时调用。

此处的解决方案是使用多线程。
为什么UDP版本的程序没用多线程,也是可以的?
UDP不需要处理连接,UDP只需要一个循环 ,就可以处理所有客户端的额请求。但是,TCP既要处理连接,又要处理一个连接中的若干次请求,就需要两个循环,里层循环就会影响到外层循环的进度了。

主线程,循环调用accept,当有客户端连接上来的时候,就直接让主线程创建一个新线程,由新线程负责对客户端的若干个请求提供服务(在新线程里,通过while循环来处理请求),这个时候多个线程是并发执行的关系(宏观上看起来同时执行)就是各自执行各自的了,就不会互相干扰了。(每个客户端连接上来的时候,都要分配一个线程)

改进之后:
1.只改外层循环

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

2.使用线程池

 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();
            //【改进方法】使用线程池
           pool.submit(new Runnable() {
               @Override
               public void run() {
                   processConnection(clientSocket);
               }
           });
        }
    }

翻译程序(英译汉)

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

    public TcpDictServer(int port) throws IOException {
        super(port);

        dict.put("cat","小猫");
        dict.put("dog","小狗");
        dict.put("monkey","小猴");
        dict.put("pig","小猪");

    }

    @Override
    public  String process (String request){
        return dict.getOrDefault(request,"当前的词无法解释");
    }

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

}

执行效果
在这里插入图片描述

一个TCP服务器,能否让一个UDP客户端连上?
不能。TCP和UDP无论是API代码,还是协议底层的工作过程,都是差异巨大的。
一次通信,需要用到五元组,协议类型不匹配,通信是无法完成的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值