【Java基础】网络编程-TCP编程Demo

一个客户端对一个服务端

  1. 客户端与服务端一直保持socket连接通过控制台循环交互
  2. 具体表现为客户端发起请求,服务端接受客户端请请求并在控制台输入响应, 客户端接受服务端响应, 循环进行以上步骤

服务端

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

/**
 * @Description TODO socket客户端
 * @Author JianPeng OuYang
 * @Date 2020/1/21 18:12
 * @Version v1.0
 */
//        1. 服务端:创建ServerSocket对象,绑定监听端口
//        2. 服务端:通过accept()方法监听客户端请求
//        3. 客户端:创建Socket对象,指明需要连接的服务器的地址和端口号
//        4. 客户端:连接建立后,通过输出流向服务器发送请求信息
//        5. 服务端:连接建立后,通过输入流读取客户端发送的请求信息
//        6. 服务端:通过输出流向客户端发送响应信息
//        7. 客户端:通过输入流获取服务器相应的信息
//
//        - 客户端、服务器端都使用Socket中的getInputStream方法和getOutputStream方法获得输入流和输出流,进一步进行数据读写操作
public class SocketClient {
    private static String HOST = "127.0.0.1";
    private static int PROT = 8080;
    private static String CHARSET = "utf-8";


    public static void main(String[] args) {
        BufferedWriter bw = null;
        BufferedReader br =  null;

        try {
            Socket clientSocket = new Socket(HOST, PROT);
            System.out.println("客户端初始化。。。。。");

            // 获取连接服务端的输入输出流,用于向服务器提交数据或者获取响应
             bw = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream(), CHARSET));
             br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), CHARSET));


            //放在这里表示5秒内客户端与服务端已经建立连接,但5秒内没有读取到服务端响应,则会抛出异常
            //clientSocket.setSoTimeout(5000);

             // 在一次连接中循环与服务端进行交互
            while (true) {
                //客户端发起请求
                System.out.print("客户端发起请求=>");
                Scanner scanner = new Scanner(System.in);
                String request = scanner.nextLine();

                bw.write(request);
                //因为在服务端使用的是readLine,所以如果不调用newLine,那么会一直阻塞
                bw.newLine();
                bw.flush();

                //clientSocket.shutdownOutput();
                String reqponse = br.readLine();// read()和readLine()都会读取对端发送过来的数据,如果无数据可读,就会阻塞直到有数据可读。或者到达流的末尾,这个时候分别返回-1和null。
                System.out.println("客户端接受响应=>"+reqponse);

                if(request.equals("exit")){
                    System.out.println("客户端关闭连接");
                    clientSocket.close();
                    break;
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //这里不能随便关闭流,否则会把socket也关闭了(因为后面还要发送数据,所以不能关闭流,不管是关闭输入输入其中之一,都会导致输入和输出都不能使用)
            // 我这里是因为使用了循环,不手动结束循环不会走finally
            try {
                if (bw != null) {
                    bw.close();
                }
                if (br != null) {
                    br.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

客户端

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

/**
 * @Description TODO socket客户端
 * @Author JianPeng OuYang
 * @Date 2020/1/21 18:12
 * @Version v1.0
 */
//        1. 服务端:创建ServerSocket对象,绑定监听端口
//        2. 服务端:通过accept()方法监听客户端请求
//        3. 客户端:创建Socket对象,指明需要连接的服务器的地址和端口号
//        4. 客户端:连接建立后,通过输出流向服务器发送请求信息
//        5. 服务端:连接建立后,通过输入流读取客户端发送的请求信息
//        6. 服务端:通过输出流向客户端发送响应信息
//        7. 客户端:通过输入流获取服务器相应的信息
//
//        - 客户端、服务器端都使用Socket中的getInputStream方法和getOutputStream方法获得输入流和输出流,进一步进行数据读写操作
public class SocketClient {
    private static String HOST = "127.0.0.1";
    private static int PROT = 8080;
    private static String CHARSET = "utf-8";


    public static void main(String[] args) {
        BufferedWriter bw = null;
        BufferedReader br =  null;

        try {
            Socket clientSocket = new Socket(HOST, PROT);
            System.out.println("客户端初始化。。。。。");

            // 获取连接服务端的输入输出流,用于向服务器提交数据或者获取响应
             bw = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream(), CHARSET));
             br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), CHARSET));


            //放在这里表示5秒内客户端与服务端已经建立连接,但5秒内没有读取到服务端响应,则会抛出异常
            //clientSocket.setSoTimeout(5000);

             // 在一次连接中循环与服务端进行交互
            while (true) {
                //客户端发起请求
                System.out.print("客户端发起请求=>");
                Scanner scanner = new Scanner(System.in);
                String request = scanner.nextLine();

                bw.write(request);
                //因为在服务端使用的是readLine,所以如果不调用newLine,那么会一直阻塞
                bw.newLine();
                bw.flush();

                //clientSocket.shutdownOutput();
                String reqponse = br.readLine();// read()和readLine()都会读取对端发送过来的数据,如果无数据可读,就会阻塞直到有数据可读。或者到达流的末尾,这个时候分别返回-1和null。
                System.out.println("客户端接受响应=>"+reqponse);

                if(request.equals("exit")){
                    System.out.println("客户端关闭连接");
                    clientSocket.close();
                    break;
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //这里不能随便关闭流,否则会把socket也关闭了(因为后面还要发送数据,所以不能关闭流,不管是关闭输入输入其中之一,都会导致输入和输出都不能使用)
            // 我这里是因为使用了循环,不手动结束循环不会走finally
            try {
                if (bw != null) {
                    bw.close();
                }
                if (br != null) {
                    br.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

服务端与客户端初始化
在这里插入图片描述
在这里插入图片描述
客户端发起请求
在这里插入图片描述
服务端接受请求并通过控制台向客户端输出响应数据
在这里插入图片描述
客户端与服务端的socket连接并没有断开,因此客户端与服务端可以持续交互
在这里插入图片描述

一个服务器对多个客户端

  1. 服务端每次连接成功一个客户端,则启动一个线程为其服务
  2. 客户端与服务端请求响应结束之后,分别断开连接,具体表现为“一次请求-一次响应”
    服务端
  • 可以看到上面的服务器端程序和客户端程序是一对一的关系,为了能让一个服务器端程序能同时为多个客户提供服务,可以使用多线程机制,每个客户端的请求都由一个独立的线程进行处理。下面是改写后的服务器端程序。
import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
//


/*在网络中,我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程.而socket编程就是为了完成两个唯一进程之间的通信(一个是客户端,一个是服务器端),其中用到的协议是TCP/UDP协议,它们都属于传输层的协议.

        TCP是基于连接的协议,在收发数据前,需要建立可靠的连接,也就是所谓的(三次握手).使用TCP协议时,数据会准确到达,但是效率较低.

        UDP是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发送过去.使用UDP协议时,传输效率高,但是不能保证数据准确到达,视频聊天,语音聊天时就用的UDP协议.

        以使用TCP协议通讯的socket为例,其交互流程大概是这样子的:

        服务器端
        创建服务器端的socket
        绑定端口号
        监听端口
        接收客户端的连接请求
        读取客户端发送数据
        关闭socket

        客户端
        创建客户端的socket
        连接服务器端的端口
        向服务器端发送数据
        关闭socket*/
/**
 * @Description TODO     单一服务器对多客户端    服务端:每次连接成功一个客户端,则启动一个线程为其服务,一次请求一次响应
 * @Author JianPeng OuYang
 * @Date 2020/1/26 13:58
 * @Version v1.0
 */
public class ServiceSocket {
    private static int PROT = 8080;
    private static String CHARSET = "UTF-8";

    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(8080);//创建绑定到特定端口的服务器Socket.
            Socket socket = null;//需要接收的客户端Socket
            int count = 0;//记录客户端数量
            System.out.println("服务器启动");
            //定义一个死循环,不停的接收客户端连接
            while (true) {
                socket = serverSocket.accept();//侦听并接受到此套接字的连接
                InetAddress inetAddress=socket.getInetAddress();//获取客户端的连接
                ServerThread thread=new ServerThread(socket,inetAddress);//自己创建的线程类
                thread.start();//启动线程
                count++;//如果正确建立连接
                System.out.println("客户端数量:" + count);//打印客户端数量
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}




/**
*处理客户端请求的线程
*/
 class ServerThread extends Thread {
    Socket socket = null;
    InetAddress inetAddress=null;//接收客户端的连接

    public ServerThread(Socket socket,InetAddress inetAddress) {
        this.socket = socket;
        this.inetAddress=inetAddress;
    }

    @Override
    public void run() {

        BufferedReader br = null;//为输入流添加缓冲
        BufferedWriter  bw = null;//为输出流添加缓冲
        try {
            br =  new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));

            String info = null;//临时
            //循环读取客户端信息
            while ((info = br.readLine()) != null) {
                //获取客户端的ip地址及发送数据
                System.out.println("服务器端接收:"+"{'from_client':'"+socket.getInetAddress().getHostAddress()+"','data':'"+info+"'}");
            }
           socket.shutdownInput();//关闭输入流

            //响应客户端请求
            bw =  new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));

            bw.write("{'to_client':'"+inetAddress.getHostAddress()+"','data':'我是服务器数据'}");
            bw.newLine();
            bw.flush();//清空缓冲区数据

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭资源
            try {
                CloseUtil.closeAll(bw,br);
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}
accept()方法
  1. 注意到代码accept()表示每当有新的客户端连接进来后,就返回一个Socket实例,这个Socket实例就是用来和刚连接的客户端进行通信的。由于客户端很多,要实现并发处理,我们就必须为每个新的Socket创建一个新线程来处理,这样,主线程的作用就是接收新的连接,每当收到新连接后,就创建一个新线程进行处理。

  2. 如果没有客户端连接进来,accept()方法会阻塞并一直等待。如果有多个客户端同时连接进来,ServerSocket会把连接扔到队列里,然后一个一个处理。对于Java程序而言,只需要通过循环不断调用accept()就可以获取新的连接。

  3. 这里可以利用线程池来处理客户端连接,能大大提高运行效率。

客户端

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

/**
 * @Description TODO   单一服务器对多客户端    服务端:每次连接成功一个客户端,则启动一个线程为其服务
 * @Author JianPeng OuYang
 * @Date 2020/1/26 13:58
 * @Version v1.0
 */
public class ClientSocket {
    private static String HOST = "127.0.0.1";
    private static int PROT = 8080;
    private static String CHARSET = "utf-8";

    public static void main(String[] args) {
        try {
            Socket socket = new Socket(HOST, PROT);

            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(), CHARSET));
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), CHARSET));
            Scanner scanner = new Scanner(System.in);

            System.out.println("请输入数据:");
            String data = scanner.nextLine();
            bw.write(data);
            bw.flush();//刷新缓冲
            socket.shutdownOutput();//只关闭输出流而不关闭连接
            //获取服务器端的响应数据

            String info = null;
            System.out.println("客户端IP地址:" + socket.getInetAddress().getHostAddress());
            //输出服务器端响应数据
            while ((info = br.readLine()) != null) {
                System.out.println("客户端接收:" + info);
            }

            CloseUtil.closeAll(br,bw);
            //关闭资源
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

关闭IO工具类

import java.io.Closeable;
import java.io.IOException;

/**
 * @Description TODO
 * @Author JianPeng OuYang
 * @Date 2020/1/26 13:59
 * @Version v1.0
 */
public class CloseUtil {
    public static void closeAll(Closeable... arr) {
        if (arr == null) {
            return;
        }

        try {
            for (Closeable cloneable : arr) {
                cloneable.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服务端初始化
在这里插入图片描述

客户端初始化并与服务端建立连接
在这里插入图片描述
建立连接
在这里插入图片描述

客户端发起请求并接收服务端响应后断开与服务端连接,表示一次请求响应结束
在这里插入图片描述

服务端响应结束后等待其他连接
在这里插入图片描述
上面改进后的服务器端代码可以支持不断地并发响应网络中的客户请求。关键的地方在于多线程机制的运用,同时利用线程池可以改善服务器程序的性能。

一个服务器对多个客户端的基于控制台的聊天室

服务端

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

/**
 * @Description TODO socket服务端
 * @Author JianPeng OuYang
 * @Date 2020/1/21 18:12
 * @Version v1.0
 */

//        1. 服务端:创建ServerSocket对象,绑定监听端口
//        2. 服务端:通过accept()方法监听客户端请求
//        3. 客户端:创建Socket对象,指明需要连接的服务器的地址和端口号
//        4. 客户端:连接建立后,通过输出流向服务器发送请求信息
//        5. 服务端:连接建立后,通过输入流读取客户端发送的请求信息
//        6. 服务端:通过输出流向客户端发送响应信息
//        7. 客户端:通过输入流获取服务器相应的信息
//
//        - 客户端、服务器端都使用Socket中的getInputStream方法和getOutputStream方法获得输入流和输出流,进一步进行数据读写操作
public class SocketServer {
    private static String CHARSET = "utf-8";
    private List<MyChannel> channelList = new ArrayList<>();

    public static void main(String[] args) {
        new SocketServer().start();
    }

    public void start() {
        try {
            ServerSocket serverSocket = new ServerSocket(8080);

            while (true) {
                //放在这里表示5秒内没有客户端与服务端已经建立连接,则会抛出异常
                //serverSocket.setSoTimeout(5000);
                System.out.println("服务端初始化。。。。。");

                // 监听客户端请求
                Socket client = serverSocket.accept();

                MyChannel channel = new MyChannel(client);
                channelList.add(channel);//统一管理
                new Thread(channel).start(); //一条道路

                //放在这里表示客户端与服务端已经建立连接,5秒内客户端没有发起请求,则会抛出异常
                //socket.setSoTimeout(5000)
            }

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

    /**
     * 一个客户端 一条道路
     * 1、输入流
     * 2、输出流
     * 3、接收数据
     * 4、发送数据
     */
    private class MyChannel implements Runnable {
        private BufferedReader br = null;
        private BufferedWriter bw = null;
        private boolean isRunning = true;
        private String name = null;

        public MyChannel(Socket client) {
            //服务器端都使用Socket中的getInputStream方法和getOutputStream方法获得输入流和输出流,进一步进行数据读写操作
            try {
                br = new BufferedReader(new InputStreamReader(client.getInputStream(), CHARSET));
                bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(), CHARSET));
                this.name = br.readLine();
                this.send("欢迎您进入聊天室");
                sendOtherClients(this.name + "进入了聊天室", true);
            } catch (IOException e) {
                isRunning = false;
                CloseUtil.closeAll(bw, br);
            }
        }

        /**
         * 发送数据
         *
         * @param msg
         */
        private void send(String msg) {
            try {
                bw.write(msg);
                bw.newLine();
                bw.flush();
            } catch (IOException e) {
                isRunning = false;
                CloseUtil.closeAll(bw, br);
                channelList.remove(this);//移除自身
            }
        }

        /**
         * 接收数据
         *
         * @return
         */
        private String receive() {
            String msg = "";
            try {
                msg = br.readLine();
                return msg;
            } catch (IOException e) {
                isRunning = false;
                CloseUtil.closeAll(bw, br);
                channelList.remove(this);//移除自身
            }
            return msg;
        }

        /**
         * 发送给其他客户端
         */
        private void sendOtherClients(String msg, boolean sys) {
            if (msg.startsWith("@") && msg.indexOf(":") != -1) {
                //获取name
                String name = msg.substring(1, msg.indexOf(":"));
                String content = msg.substring(msg.indexOf(":") + 1);

                for (MyChannel other : channelList) {
                    if (other.name.equals(name)) {
                        other.send(this.name + "对您悄悄地说:" + content);
                    }
                }
            } else {
                //遍历容器
                for (MyChannel other : channelList) {
                    if (other == this) {
                        continue;
                    }
                    if (sys) { //系统信息
                        other.send("系统信息:" + msg);
                    } else {
                        //发送其他客户端
                        other.send(this.name + "对所有人说:" + msg);
                    }
                }
            }
        }


        @Override
        public void run() {
            while (isRunning) {
                sendOtherClients(receive(), false);
            }
        }
    }
}

客户端

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;

/**
 * @Description TODO socket客户端
 * @Author JianPeng OuYang
 * @Date 2020/1/21 18:12
 * @Version v1.0
 */
//        1. 服务端:创建ServerSocket对象,绑定监听端口
//        2. 服务端:通过accept()方法监听客户端请求
//        3. 客户端:创建Socket对象,指明需要连接的服务器的地址和端口号
//        4. 客户端:连接建立后,通过输出流向服务器发送请求信息
//        5. 服务端:连接建立后,通过输入流读取客户端发送的请求信息
//        6. 服务端:通过输出流向客户端发送响应信息
//        7. 客户端:通过输入流获取服务器相应的信息
//
//        - 客户端、服务器端都使用Socket中的getInputStream方法和getOutputStream方法获得输入流和输出流,进一步进行数据读写操作
public class SocketClient {
    private static String HOST = "127.0.0.1";
    private static int PROT = 8080;

    public static void main(String[] args) {
        try {
            System.out.println("请输入名称:");
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            String name = br.readLine();
            if(name.equals("")){
                return;
            }


            Socket clientSocket = new Socket(HOST, PROT);
            System.out.println("客户端初始化。。。。。");
            new Thread(new Send(clientSocket,name)).start();
            new Thread(new Receive(clientSocket)).start();

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

客户端发送数据线程

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;

/**
 * 发送线程
 *
 * @author Administrator
 */
public class Send implements Runnable {
    private BufferedWriter bw = null;
    private Boolean isRunning = true;
    private String name;

    public Send(Socket client, String name) {
        try {
            this.name = name;
            this.bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(), "UTF-8"));
            send(name);
        } catch (IOException e) {
            CloseUtil.closeAll(bw);
            isRunning = false;
        }
    }

    private String getMsgFromConsole() {
        Scanner scanner = new Scanner(System.in);
        String request = scanner.nextLine();
        return request;
    }

    public void send(String msg) {
        try {
            if (msg != null && !"".equals(msg)) {
                bw.write(msg);
                bw.newLine();
                bw.flush();
                System.out.println(Thread.currentThread().getName() + "=>send(" + msg + ")");
            }
        } catch (IOException e) {
            isRunning = false;
            CloseUtil.closeAll();
        }
    }

    @Override
    public void run() {
        while (isRunning) {
            send(getMsgFromConsole());
        }
    }
}

客户端接受数据线程

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;

/**
 * 接收线程 
 * @author Administrator
 *
 */
public class Receive implements Runnable {
	private BufferedReader br = null;
	private Boolean isRunning = true;

	public Receive(Socket client) {
		try {
			this.br = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8"));
		} catch (IOException e) {
			CloseUtil.closeAll(br);
			isRunning = false;
		}
	}

	private String receive() {
		try {
			String response = br.readLine();
			return response;
		} catch (IOException e) {
			CloseUtil.closeAll(br);
			isRunning = false;
		}

	return  null;
	}



	@Override
	public void run() {
		while (isRunning) {
			System.out.println(Thread.currentThread().getName()+"=>receive():"+receive());
		}
	}
}

关闭IO工具类

import java.io.Closeable;
import java.io.IOException;

/**
 * @Description TODO
 * @Author JianPeng OuYang
 * @Date 2020/1/26 13:59
 * @Version v1.0
 */
public class CloseUtil {
    public static void closeAll(Closeable... arr) {
        if (arr == null) {
            return;
        }

        try {
            for (Closeable cloneable : arr) {
                cloneable.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服务端初始化,等待客户端发起连接
在这里插入图片描述
客户端初始化连接服务端,并创建张三用户

在这里插入图片描述
在这里插入图片描述

客户端初始化连接服务端,并创建李四用户
在这里插入图片描述
在这里插入图片描述

张三非当前用户发起聊天
在这里插入图片描述
在这里插入图片描述

李四对张三私发一条消息

在这里插入图片描述
在这里插入图片描述

Socket流
当Socket连接创建成功后,无论是服务器端,还是客户端,我们都使用Socket实例进行网络通信。因为TCP是一种基于流的协议,因此,Java标准库使用InputStream和OutputStream来封装Socket的数据流,这样我们使用Socket的流,和普通IO流类似:

为什么写入网络数据时,要调用flush()方法。

  • 如果不调用flush(),我们很可能会发现,客户端和服务器都收不到数据,这并不是Java标准库的设计问题,而是我们以流的形式写入数据的时候,并不是一写入就立刻发送到网络,而是先写入内存缓冲区,直到缓冲区满了以后,才会一次性真正发送到网络,这样设计的目的是为了提高传输效率。如果缓冲区的数据很少,而我们又想强制把这些数据发送到网络,就必须调用flush()强制把缓冲区数据发送出去。

小结

使用Java进行TCP编程时,需要使用Socket模型:

  • 服务器端用ServerSocket监听指定端口;
  • 客户端使用Socket(InetAddress, port)连接服务器;
  • 服务器端用accept()接收连接并返回Socket;
  • 双方通过Socket打开InputStream/OutputStream读写数据;
  • 服务器端通常使用多线程同时处理多个客户端连接,利用线程池可大幅提升效率;
  • flush()用于强制输出缓冲区到网络。

java网络编程入门-模拟文件上传与服务器

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章