上篇文章:
网络编程—Socket套接字(UDP)https://blog.csdn.net/sniper_fandc/article/details/146923670?fromshare=blogdetail&sharetype=blogdetail&sharerId=146923670&sharerefer=PC&sharesource=sniper_fandc&sharefrom=from_link
目录
1 TCP流套接字
2 模拟实现TCP服务器
1 TCP流套接字
基于TCP的Socket主要有:ServerSocket和Socket,ServerSocket用于创建TCP服务器端的Socket,而Socket用于创建TCP客户端的Socket。操作方式也类似文件。
构造方法/方法 | 含义 |
ServerSocket(int port) | 构造方法,创建一个服务端流套接字Socket,并绑定到指定端口 |
Socket accept() | 普通方法,开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭TCP套接字 |
因为TCP是面向流的数据读写方式,因此没有像DatagramPacket数据报的API,只需创建Socket后,采用类似InputStream和OutputStream的操作方式。也可以对InputStream和OutputStream进行Scanner和PrintWriter的包装,便于字符数据的读写。
构造方法/方法 | 含义 |
Socket(String host, int port) | 构造方法,创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
InetAddress getInetAddress() | 从套接字中获取连接的IP地址 |
InputStream getInputStream() | 返回套接字中的输入流(读请求) |
OutputStream getOutputStream() | 返回套接字中的输出流(写响应) |
注意:Socket可能有两种获得的方式,1是使用Socket构造方法,2是使用ServerSocket的方法accept()。也就是说ServerSocket的主要作用就是创建TCP服务器的全局连接监听,客户端作为连接发起方,因此直接创建Socket表示申请建立连接,而ServerSocket的accept()方法一旦监听到有客户端申请建立连接,就返回一个Socket用于建立服务器和客户端之间的连接。
上述分析方式也透露了ServerSocket和Socket的生命周期,ServerSocket的生命周期伴随整个服务器进程,而Socket的生命周期只是一次连接周期。
2 模拟实现TCP服务器
public class TcpServer {//服务器端口号private final int PORT = 8000;//创建服务器private ServerSocket serverSocket = null;public TcpServer() throws IOException {serverSocket = new ServerSocket(PORT);}//启动服务器public void start() throws IOException {System.out.println("服务器启动成功");ExecutorService executorService = Executors.newCachedThreadPool();while(true){//将建立的TCP连接拿到应用程序中(accept()会阻塞,直到建立连接)Socket clientSocket = serverSocket.accept();//[版本1]直接调用processConnect()就会导致第一个客户端连接执行到该方法while中,服务器线程从而无法执行accept//进而无法一个服务器为多个客户端服务//[版本2]解决方案:多线程(一个线程accept(),一个线程processConnect())(新的问题:频繁创建销毁线程)// Thread t = new Thread(() ->{// try {// processConnect(clientSocket);// } catch (IOException e) {// e.printStackTrace();// }// });// t.start();//[版本3]解决方案:线程池(新的问题:线程数量太多了(IO多路复用->NIO))executorService.submit(new Runnable() {@Overridepublic void run() {try {processConnect(clientSocket);} catch (IOException e) {e.printStackTrace();}}});}}//给当前连接的客户端提供服务(一个连接只进行一次数据交互服务(短连接)||一个连接进行多次数据交互服务(长连接))//长连接版本(去掉循环就是短连接版本)public void processConnect(Socket clientSocket) throws IOException {System.out.printf("[%s:%d] 建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()){Scanner scanner = new Scanner(inputStream);PrintWriter printWriter = new PrintWriter(outputStream);while(true){if(!scanner.hasNext()){//如果没有请求说明客户端断开连接System.out.printf("[%s:%d] 断开连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());break;}//1.读取请求并解析String request = scanner.next();//2.根据请求计算响应String response = process(request);//3.响应写回客户端// (注意此处不能使用next()类的函数,因为这类函数读取结束标志是空白符:换行符、回车符等,输入没有这些符号服务器就会被阻塞在这类函数)printWriter.println(response);//刷新一下缓冲区printWriter.flush();System.out.printf("[%s:%d] request:%s, response:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);}}finally {//连接用完需要关闭(clientSocket生命周期是一次连接周期,而serverSocket生命周期是整个服务器运行周期)clientSocket.close();}}public String process(String request) {return request;}public static void main(String[] args) throws IOException {TcpServer tcpServer = new TcpServer();tcpServer.start();}}public class TcpClient {//创建客户端private Socket socket = null;public TcpClient() throws IOException {//new对象时就是和TCP服务器建立连接(因此需要直到服务器地址)socket = new Socket("127.0.0.1",8000);}//启动服务器public void start() throws IOException {Scanner scanner = new Scanner(System.in);try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()){Scanner scannerNet = new Scanner(inputStream);PrintWriter printWriter = new PrintWriter(outputStream);while(true){//1.读取用户输入System.out.print(">");//注意此时next()读取到换行就结束了,但是读取的数据不含空白符,即没有回车符String request = scanner.next();//2.发送请求// (注意此处不能使用next()类的函数,因为这类函数读取结束标志是空白符:换行符、回车符等,输入没有这些符号服务器就会被阻塞在这类函数)printWriter.println(request);printWriter.flush();//3.接收响应String response = scannerNet.next();//4.将响应返回给用户System.out.printf("request:%s, response:%s\n",request,response);}}}public static void main(String[] args) throws IOException {TcpClient tcpClient= new TcpClient();tcpClient.start();}}
运行结果如下:
上述代码需要注意3点:
1.服务器端什么时候该关闭clientSocket(即关闭连接)?当服务器端processConnect方法内部从循环跳出时,证明此时客户端没有数据要发送,此时可以关闭连接,采用try-catch-finally方式,防止出现异常无法正常关闭。
2.如何处理next()引起的阻塞问题?上述代码很多地方可能要用到Scanner的next()方法,但是该方法会读取到空白符(回车换行等)才能结束,当客户端输入数据时可能不会携带空白符(在命令行中敲回车,该回车会被接收数据的next识别,发送的请求中并不携带回车符),此时就会导致服务器端一直未识别到结束,从而一直无响应。解决的办法就是在发送的数据中添加空白符,比如使用println()方法会自动在数据结尾添加回车符。
3.如何解决服务器端只能为一个客户端服务?当不采用多线程方案时,第一个客户端建立连接发送请求,进入processConnect方法内部时,服务器端的主线程就会进入while中,从而其他客户端申请建立连接时,服务器主线程无法通过accept()监听建立连接的申请。采用多线程方案,线程池实现一个线程为一个客户端服务(注意,当并发量很大时,线程池的线程数量很多,就会导致资源浪费调度困难等问题,此时需要采用NIO(非阻塞IO)的方式,这是一种I/O多路复用的技术,可以实现一个线程管理多个客户端)。
下篇文章:
网络编程—TCP/IP模型(UDP协议与自定义协议)https://blog.csdn.net/sniper_fandc/article/details/146923934?fromshare=blogdetail&sharetype=blogdetail&sharerId=146923934&sharerefer=PC&sharesource=sniper_fandc&sharefrom=from_link