Java中的Socket(套接字)

目录

socket(套接字)

TCP与UDP区别:

有无连接:

可靠传输与不可靠传输:

面向数据报与面向字节流:

全双工与半双工:

UDP的相关api:

DatagramSocket API:

构造方法:

其他方法:

DatagramPacket API:

构造方法:

其他方法:

使用UDP的api写一个回显服务器与客户端:

服务器:

客户端:

TCP的相关api:

ServerSocket API:

构造方法:

其他方法:

Socket API:

构造方法:

其他方法:

使用TCP的api写一个回显服务器与客户端:

服务器:

客户端:


socket(套接字)

程序猿在进行网络编程的时候,主要编写的是应用层的代码,在传输数据的时候,需要上层协议调用下层协议,应用层需要调用传输层的一些api,而这些api统称为socket api

(操作系统提供的api是C/C++风格的api,这些api由JVM封装过,在使用时,Java程序猿使用封装过的api)

操作系统为应用层提供的api由很多很多,其中最主要的有两组,分别对应传输层UDP的api和TCP的api

TCP与UDP区别:

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

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

有无连接:

这里的连接是一个抽象概念,并不是像绳子一样连接

这里的连接指的是有无记录,如果传输双端互相记录了对方,就叫有链接

比如发送短线验证码只是根据人为输入的电话号码发送短信,服务器并没有记录你的电话号码。直接投送短信,这就是无连接

而打电话时必须先通过记录的双发信息让电话联通,才能通话,即必须先记录连接才能信息传输,如果连接无法建立就不能信息交互,这就是有连接

无连接:使用UDP通信的双方,不需要刻意保存对端的相关信息

有连接:使用TCP通信的双方,需要可以保存对方的相关信息

可靠传输与不可靠传输:

传输成功与否并不只是程序猿的责任,如果相关硬件设施出现问题,比如光纤断裂,照样传输失败

可靠传输:尽可能的传输,如果传输失败,是可以得知的

不可靠传输:只关心消息发没发送,不关心传输失败与否

面向数据报与面向字节流:

面向数据报:以一个UDP数据报为基本单位

面向字节流:以字节为传输的基本单位,读写非常灵活

全双工与半双工:

UDP的相关api:

DatagramSocket API:

Datagram就是数据报的意思,而Socket说明这个对象是一个特殊的文件,区别于普通文件与目录,socket文件不是对应在硬盘的某个文件中,而是网卡这个硬件设备中

无线网卡:

有线网卡(未连接):

想要进行网络通信,就需要socket文件这样的对象,借助socket文件对象,才能间接操控网卡

向socket对象中写数据,相当于通过网卡发送消息

从socket对象中读数据,相当于通过网卡接收消息

构造方法:

方法签名

方法说明

DatagramSocket()

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

DatagramSocket(int port)

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

客户端一般使用无参数的版本,由操作系统自动分配即可

服务端一般使用带参数的版本,服务端的端口号必须不变,以便于客户端能找得到服务端

类似于(店面位置不可变,但顾客在饭店内坐哪桌是无所谓的)

其他方法:

方法签名

方法说明

void receive(DatagramPacket p)

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

void send(DatagramPacket p)

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

void close()

关闭此数据报套接字

注意,使用完socket文件后需要关闭资源,不然会造成文件资源泄露

DatagramPacket API:

DatagramPacket是UDP Socket发送和接收的数据报

构造方法:

方法签名

方法说明

DatagramPacket(byte[]buf, int length)

构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length)

DatagramPacket(byte[]buf, int offset, int length,SocketAddress address)

构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号

第一个版本的构造方法不需要设置地址,通常用于接收消息

第二个版本的构造方法需要显式的设置地址进去,通常用于发送消息

其他方法:

方法签名

方法说明

InetAddress getAddress()

从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址

int getPort()

从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号

byte[] getData()

获取数据报中的数据

使用UDP的api写一个回显服务器与客户端:

(注意运行的时候要先运行服务器再运行客户端,因为先运行服务器,服务器会因为receive方法阻塞等待客服端的请求,如果先运行客户端,在客户端发送请求的时候,服务器没有启动则会出现问题)

服务器:

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;
    //这里可能会抛出异常,并不是给了端口号,就一定能绑定成功,
    //如果失败,其大概率原因是该端口已经被其他进程绑定了
    //同一台主机,同一时刻,一个端口只能被一个进程绑定
    public UdpEchoServer(int port) throws SocketException {
        //让socket绑定/关联相关端口
        this.socket = new DatagramSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动!");
        //服务器并不是单独为某一个客户端只服务一次的
        //可能由许多客户端,而每个客户端可能有许多请求
        //所以服务器必须使用while(true)循环,时刻准备请求的到来
        while (true){
            //在每次循环内都需要做三件事
            //1.读取请求并解析
                //这里要先创建一个数据报
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
                //然后将这个空的数据报传入receive方法中,如果socket收到客户端发送的
                //请求,那么在该方法内packet就会被得到填充,如果收不到客户端发来的请求
                //即暂时无客户端需要发送请求,那么该方法就会阻塞等待,直到收到请求
                //该方法涉及到IO操作,即在网卡中读写,所以可能会导致IO异常
            socket.receive(requestPacket);
                //为了方便解析使用请求,这里将请求变为一个字符串(该操作不是必须的,只是为了方便)
            String request = new String(requestPacket.getData(),0, requestPacket.getLength());
            //2.根据请求计算响应结果
            String respond = process(request);
            //3.把响应结果写回客户端内
                //要先打包包裹
            DatagramPacket respondPacket = new DatagramPacket(respond.getBytes(),respond.getBytes().length
                    //用于发送的包裹在初始化的时候需要获取客户端的地址
                    //可以通过接收的请求获取到地址,这个地址是一个SocketAddress对象
                    //该对象内部包含客户端的IP地址和相应的端口号
                    ,requestPacket.getSocketAddress());
            socket.send(requestPacket);
            //打印日志
            System.out.printf("[%s : %d] request: %s respond: %s\n",requestPacket.getAddress().toString()
            ,requestPacket.getPort(),request,respond);
        }
    }

    private String process(String request) {
        //这里用于计算响应结果
        //这只是一个回显程序,就省略计算了
        return request;
    }
}

客户端:

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

public class Client {
    private DatagramSocket socket = null;
    private String severIP;
    private int severPort;
    private String name;

    public Client(String severIP, int severPort,String name) throws SocketException {
        this.severIP = severIP;
        this.severPort = severPort;
        this.name = name;
        socket = new DatagramSocket();
    }

    public void start() throws IOException {
        printMessageThread();
        online();
        Scanner scanner = new Scanner(System.in);
        while (true){
            String outcome = name + " " + scanner.nextLine();
            DatagramPacket outcomePacket = new DatagramPacket(outcome.getBytes(),
                    outcome.getBytes().length, InetAddress.getByName(severIP),severPort);
            socket.send(outcomePacket);
        }
    }
    public void printMessageThread() {
        Thread thread = new Thread(() -> {
            while (true){
                DatagramPacket incomePacket = new DatagramPacket(new byte[4096],4096);
                try {
                    socket.receive(incomePacket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                String income = new String(incomePacket.getData(),0,incomePacket.getLength());
                String[] strings = income.split(" ",2);
                String name = strings[0];
                String message = strings[1];
                System.out.println(name + ":" + "\n" + "    " + message);
            }
        });
        thread.start();
    }
    public void online() throws IOException {
        String outcome = name + " " + name + "上线";
        DatagramPacket outcomePacket = new DatagramPacket(outcome.getBytes(),
                outcome.getBytes().length,InetAddress.getByName(severIP),severPort);
        socket.send(outcomePacket);
    }

    public static void main(String[] args) throws IOException {
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入昵称:");
        String name = scanner.nextLine();
        Client client = new Client("127.0.0.1",9090,name);
        client.start();
    }
}

这里的客户端与服务器内部的socket都没有调用close方法,因为这是一个简单的回显程序,内部的while(true)循环会一直执行,如果跳出了循环,即意味着start方法结束,然后main方法结束,main方法结束意味着进程结束,其会自动释放该程序占用的资源

也就是说,这里的socket的生命周期比较长,伴随整个进程,所以在整个进程结束的时候其资源会自动释放,而对于某些临时创建的DatagramSocket,其就需要手动close

如果相隔很远的两台电脑,一台启动服务器,一台启动客户端,是不能直接建立连接的,因为内网不能直接被访问,必须把客户端部署到云服务器上,拥有了外网IP,那么另一端的内网服务器才能访问到

TCP的相关api:

ServerSocket API:

主要给服务器使用

构造方法:

SeverSocket只辅助于服务器,所以必须指定端口号,就算在构造时未指定端口号,后续在使用的时候也需要指定端口号

方法签名

方法说明

ServerSocket(int port)

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

其他方法:

方法签名

方法说明

Socket accept()

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

void close()

关闭此套接字

Socket API:

构造方法:

方法签名

方法说明

Socket(String host, int port)

创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接(这里的host port分别代表服务器的ip和端口号)

其他方法:

方法签名

方法说明

InetAddress getInetAddress()

返回套接字所连接的地址

InputStream getInputStream()

返回此套接字的输入流

OutputStream getOutputStream()

返回此套接字的输出流

void close()

关闭此套接字

使用TCP的api写一个回显服务器与客户端:

服务器:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoSever {
    //SeverSocket是专用于服务器的类
    private ServerSocket serverSocket = null;

    public TcpEchoSever(int port) throws IOException {
        //专用于服务器,所以需要绑定端口号
        this.serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.printf("[%s : %d]服务器启动\n",
                serverSocket.getInetAddress().toString(),
                serverSocket.getLocalPort());
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true){
            //每次服务器收到客户端的请求时,这个请求就需要用一个Socket类对象
            //来接收,请求的内容包含在clientSocket内
            //一个clientSocket内可能包含多个请求
            //如果没有客户端连接,那么accept方法就会堵塞
            Socket clientSocket = serverSocket.accept();
            //由于processConnection方法内部也存在while(true)循环
            //所以这里直接不能直接调用processConnection,因为如果直接
            //调用,那么当前循环就会等待processConnection调用完成
            //也就是说会导致当前服务器只服务于一个客户端
            //正确方式应该是采用多线程的方式
//                Thread thread = new Thread(() -> {
//                    try {
//                        processConnection(clientSocket);
//                    } catch (IOException e) {
//                        e.printStackTrace();
//                    }
//                });
//                thread.start();
            //当然我们也可以采用线程池的方式,采用线程池比频繁的创建销毁线程更加高效
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    private void processConnection(Socket clientSocket) throws IOException {
        //这里采用长连接的形式处理clientSocket,即认为clientSocket内会包含多个请求
        try(InputStream inputStream = clientSocket.getInputStream();
        //注意 这里的input 和output 都是从clientSocket获取的资源,
        //也就是说这三部分其实是用一份资源,关闭其中之一,另外两个也会自动关闭
            OutputStream outputStream = clientSocket.getOutputStream()){
            PrintWriter printWriter = new PrintWriter(outputStream);
            //为了方便读取请求,这里做出以下规定:
                //每个请求时字符串
                //请求与请求之间采用\n的形式分割
            //这意味着我们在写入响应的时候也要遵守上述规定
            //规定不唯一,根据习惯可以采取其他方式
            Scanner scanner = new Scanner(inputStream);
            System.out.printf("[%s : %d] 客户端上线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
            while (true){
                if (!scanner.hasNext()){
                    //读到末尾了
                    System.out.printf("[%s : %d] 客户端下线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                String request = scanner.next();
                String respond = process(request);
                printWriter.println(respond);
                //由于网卡的读写速度是非常慢的,所以一般是将数据写到缓冲区内
                //等缓冲区满了的时候再将所有的数据一次性打包写到网卡内
                //也就是说其实上面的println并不是直接写到网卡内,而是写到了缓冲区
                //但是我们这里等不到缓冲区满,所以我们采取手动刷新缓冲区的方式
                //flush强行让未满的缓冲区进行一次IO操作
                printWriter.flush();
                System.out.printf("[%s : %d] request: %s respond: %s\n",clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(),request,respond);
            }

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //clientSocket只是给某一次连接进行服务的
            //在这次连接使用完毕的时候,是需要释放资源的
            clientSocket.close();
        }
    }

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

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

客户端:

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 severIp,int port) throws IOException {
        //这一步操作相当于让服务器与客户端建立TCP连接
        //连接建立成功后,服务器的accept方法就会取消阻塞等待
        this.socket = new Socket(severIp,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);
                printWriter.flush();
                //3.读取响应
                String respond = scannerFromSocket.next();
                System.out.printf("request: %s respond: %s\n",request,respond);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值