网络编程套接字socket

网络编程套接字:是操作系统给应用程序提供的一组API,叫做socket API

socket可以视为应用层和传输层之间的通信桥梁。传输层的核心协议有两种,TCP和UDP,socket API也对应着有两组

TCP和UDP的区别:

有连接:相当于打电话,得先接通,才能交互数据

无连接:相当于发微信,不需要接通,直接就能发数据

可靠传输:传输过程中,发送方道知接收方有没有收到数据

不可靠传输:传输过程中,发送方不知道接收方有没有收到数据

面向字节流:以字节为单位进行传输

面向数据报:以数据报为单位进行传输(一个数据报都会明确大小)一次发送/接收必须是一个完整的数据报

全双工:一条链路,双向通信

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

一、UDP 相关的API

主要涉及两个类DatagramSocket和DatagramPacket

DatagramSocket类表示创建了一个UDP版本的socket对象,socket对象代表着操作系统中的一个socket文件,socket文件代表着网卡硬件设备的抽象体现

DatagramSocket类的方法:

receive(DatagramPacket对象) 接收数据       

send(DatagramPacket对象)  发送数据       

close 释放资源

DatagramPacket类表示了一个UDP数据报,每次发送/接收数据,都是在传输一个DatagramPacket对象

客户端服务器程序——回显服务

出现SocketException异常原因:

1、端口号已经被占用了,就像两个人不能有同一个电话号一样,同一个主机也不能有相同的端口号

2、每个进程能够打开的文件个数是有上限的,如果进程之前已经打开了很多很多文件,就可能导致此处的socket文件不能够打开了

服务器必须手动指定端口号,客户端,既可以手动指定也可随机分配端口号

对于服务器来说,端口号要手动指定是因为后续客户端要根据这个端口来访问服务器,如果让系统随机分配,客户端就不知道服务器的端口是啥,导致不能访问

对于客户端来说,如果手动指定也行,但是随机分配更好。因为一个机器上的两个进程,不能绑定同一个端口。由于客户端上可能装了很多程序,如果手动指定一个端口,万一这个端口被其他程序占用,此时咱们的程序就不能正常工作了。而且由于客户端是主动发起请求的一方,客户端需要在发送请求之前,先知道服务器的地址+端口,但是反过来在请求发出去之前,服务器是不需要事先知道客户端的端口+地址的

//站在服务器的角度:
//源IP:服务器程序本机的IP
//源端口:服务器绑定的端口(此处手动指定了9090)
//目的IP:包含在收到的数据报中(客户端的IP)
//目的端口:包含在收到的数据报中(客户端的端口)
//协议类型:UDP
public class UdpEchoServer {//服务器
    //进行网络编程,第一步就需要先准备好socket实例,这时运行网络编程的前提
    private DatagramSocket socket=null;
    public UdpEchoServer(int port) throws SocketException {
        socket=new DatagramSocket(port);//此处在构造服务器的socket对象的时候,需要绑定一个端口号,用来区分应用程序
    }
    //启动服务器
    public void start() throws IOException {
        System.out.println("启动服务器!");
        //UDP不需要建立连接,直接接收从客户端来的数据即可
        while(true){
            //1、读取客户端发来的请求
            DatagramPacket requestPacket=new DatagramPacket(new byte[1024],1024);//DatagramPacket表示一个UDP数据报,发送一次数据就是在发一个DatagramPacket,接受一次数据就是在接受一个DatagramPacket
            socket.receive(requestPacket);//为了接收数据,需要先准备好一个空的DatagramPacket对象,由receive来进行数据填充.注意receive方法是可能会阻塞的,客户端啥时候给服务器发请求是不确定的
            //把DatagramPacket解析成一个String
            String request=new String(requestPacket.getData(),0,requestPacket.getLength(),"utf-8");
            //2、根据请求来计算响应(由于是一个回显服务,2省略)
            String response=process(request);
            //3、把响应写回到客户端    在发送数据的时候必须要指定数据发给谁,SocketAddress可视为一个类,表示要把数据发给哪个地址+端口
            DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());//response.getBytes().length拿到的是字节的个数,不能用response.length()拿到的是字符的个数
             socket.send(responsePacket);
            System.out.printf("[%s:%d] req:%s, resp:%s\n", requestPacket.getAddress().toString(), requestPacket.getPort(),request,response);
        }
    }

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

    public static void main(String[] args) throws IOException {
        UdpEchoServer server=new UdpEchoServer(9090);
        server.start();
    }
}
public class UdpEchoClient {//客户端
    private DatagramSocket socket=null;
    private String serverIP;//服务器的IP
    private int serverPort;//服务器的端口

    //站在客户端的角度:
    //源IP:本机IP
    //源端口:系统分配的端口
    //目的IP:服务器的IP
    //目的端口:服务器的端口
    //协议类型:UDP
    public UdpEchoClient(String ip,int port) throws SocketException {
        //此处的port是服务器的端口
        //客户端启动的时候,不需要给socket指定端口,客户端自己的端口是系统随机分配的
        socket=new DatagramSocket();//客户端在构造socket对象的时候,就不再手动指定端口号,使用无参的构造方法,这里不指定端口号,操作系统会自己分配一个空闲的端口号
        serverIP=ip;
        serverPort=port;
    }
    public void start() throws IOException {
        Scanner scanner=new Scanner(System.in);
        while(true){
            //1、先从控制台读取用户输入的字符串
            System.out.print("-> ");
            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 {
        //由于服务器和客户端之间在同一个机器上,使用的IP仍然是127.0.0.1,如果是不同机器上,就需要更改这里的IP
        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("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 相关的API

主要涉及两个类ServerSocket和Socket

ServerSocket:是专门给TCP服务器用的

Socket:即给服务器用,又给客户端用

客户端服务器程序——回显服务

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;
                    }
                    //此处的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();
            }
        }
    }

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

    public static void main(String[] args) throws IOException {
        TcpEchoServer server=new TcpEchoServer(9090);
        server.start();
    }
}
public class TcpEchoClient {
    //用普通的socket即可,不用ServerSocket了
    //此处不用手动给客户端指定端口号,让系统自由分配
    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();
    }
}

由于上面写的服务器不能同时让多个客户端使用,因为每次都需要建立连接,第一个客户端没执行完第二个客户端不能执行,因此需要改进~

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

带多线程版本的客户端服务器程序——回显服务

public class TcpThreadEchoServer {
    private ServerSocket serverSocket=null;
    public TcpThreadEchoServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true){
            //由于TCP是有连接的,不能一上来就读数据,而要先建立连接(接电话)
            Socket clientSocket=serverSocket.accept();
            //[改进方法]在这个地方,每次accept成功,都创建一个新的线程,由新的线程负责执行这个processConnection方法
            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(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 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 {
        TcpThreadEchoServer server=new TcpThreadEchoServer(9090);
        server.start();
    }
}

线程池版本的客户端服务器程序——回显服务

public class TcpThreadPollEchoServer {
    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是有连接的,不能一上来就读数据,而要先建立连接(接电话)
            Socket clientSocket=serverSocket.accept();
            //【改进方法】通过线程池来实现
            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());
        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 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();
    }
}

翻译程序(英译汉)

public class TcpDictServer extends TcpThreadPollEchoServer{
    private HashMap<String,String> dict=new HashMap<>();
    public TcpDictServer(int port) throws IOException {
        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 {
        TcpDictServer server=new TcpDictServer(9090);
        server.start();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值