ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

网络编程(二)TCP回显服务器与客户端通信详解

网络编程(二)TCP回显服务器与客户端通信详解 前言hello hello这里是洋不写bug~欢迎大家点赞关注收藏这篇博客会解析基于TCP协议来进行网络编程进行客户端和服务器之间的交互TCP传输的是字节流TCP通信时客户端和服务器之间要建立一条连接通道TCP的代码涉及到文件资源释放、以及多客户端访问时线程的分配相对UDP来说会稍微复杂一些初学的铁汁对于服务器和客户端交互的代码可能会感觉比较抽象建议多写几次代码结合api解析和数据传输流程细品下个人主页洋不写bug的博客所属专栏JavaEE学习铁汁们对于JavaEE的各种常用核心语法都可以在上面的前端专栏学习专栏正在持续更新中有问题可以写在评论区或者私信我哦~1TCP回显服务器/客户端TCP编程中有两个核心的类ServerSocket类创建一个这样的对象就相当于打开了一个socket文件这个socket对象是专门给服务器使用的这个类不负责发送接收主要负责建立连接Socket类创建一个这样的对象就相当于打开了一个socket文件这个类负责发送接收数据服务器和客户端都会使用tcp是字节流读写的时候是以字节为基本单位的tcp读写的代码跟前面文件IO中字节流读写文件的代码是一致的用inputStream/outputStream服务器和客户端进行通信流程跟UDP是一样如下图只是api有区别1服务器服务器代码逻辑如下ServeSocket类是专门给服务器建立连接用的构造方法中传入端口号创建ServeSocket对象在start中写个while(true)循环因为服务器要多次处理来自客户端的请求调用serveSocket的accept方法等待客户端的连接没有客户端连接时accept方法会处于阻塞状态如果有客户端访问服务器了accept方法就返回Socket对象代表服务器与该客户端之间的一条 TCP 通道后续操作数据都是通过返回的这个socketserveSocket就不参与了例如一些售楼部会雇用一些大妈专门在大街上拉人去售楼部看房大妈把客户拉到售楼部交给售楼部专业的置业顾问给客户详细的讲解房子接着大妈再去大街上拉人serveSocket就相当于大妈只负责把顾客拉到售楼部去也就是建立连接socket就类似于置业顾问连接后的操作比如说详细介绍房子、讲价、签合同是置业顾问负责的packagenetwork.tcp;importjava.io.IOException;importjava.net.ServerSocket;importjava.net.Socket;publicclassEchoServer{privateServerSocketserverSocket;publicEchoServer(intport)throwsIOException{serverSocketnewServerSocket(port);}publicvoidstart()throwsIOException{System.out.println(服务器启动);while(true){SocketsocketserverSocket.accept();processConnection(socket);}}privatevoidprocessConnection(Socketsocket){}}processConnection方法中写服务器和客户端建立连接后进行通讯的详细过程TCP协议服务器和客户端之间的通信是通过字节流的字节输出流是向文件中写数据的字节输入流是从文件中读取数据的那流程就是客户端通过输出流往socket文件中写入数据服务器通过输入流读取数据进行响应后再通过输出流把数据写到socket文件中也就是返回给客户端结合文件IO的知识服务器就可以用字节数组来读取客户端发送的数据把数据转化为字符串进行处理处理后要把数据再转化为字节数组类型用输出流把数据返回给客户端用这种方法来写代码就会涉及到很多的字节数组和字符串的相互转化有个更简单的方法就可以创建个Scanner对象和PrintWriter对象分别负责输入和输出平常用new Scanner(System.in)就是从控制台读取用户的输入现在写成new Scanner(inputStream)就是让Scanner从inputStream中读取数据后面直接用scanner.next()来获取数据即可如果客户端传输的是int类型的数据就用scanner.nextInt()来读取数据特别方便PrintWriter就是将字符串编码为字节写入 Socket 的字节输出流 OutputStream中写入时用的是println方法用System.out调用println方法就是往控制台上输出PrintWriter调用就是往OutputStream上输出PrintWriter将字符串编码成字节后 是先放到 PrintWriter 的内存缓冲区不会立刻交给 OutputStream在发送完成后还需要用flush刷新下也可以创建PrintWriter对象时传入一个true将PrintWriter设置为自动刷新写个if语句scanner.hasNext()判断下客户端是否已经关闭如果客户端已经关闭了就打印客户端下线信息退出processConnection方法进入start方法中等待下一次客户端的连接可能有的铁汁会这样想如果客户端连接上了但是客户端还没有输入此时scanner.hasNext()的返回值不就是false吗那就直接显示客户端下线退出processConnection方法吗如果客户端连接上不输入scanner.hasNext()就会阻塞等待输入数据有收到数据才返回 true只有客户端正常关闭流才返回 falsepackagenetwork.tcp;importjava.io.IOException;importjava.io.InputStream;importjava.io.OutputStream;importjava.io.PrintWriter;importjava.net.ServerSocket;importjava.net.Socket;importjava.util.Scanner;publicclassEchoServer{privateServerSocketserverSocket;publicEchoServer(intport)throwsIOException{serverSocketnewServerSocket(port);}publicvoidstart()throwsIOException{System.out.println(服务器启动);while(true){SocketsocketserverSocket.accept();processConnection(socket);}}privatevoidprocessConnection(Socketsocket){System.out.printf([%s:%d] 客户端上线\n,socket.getInetAddress().toString(),socket.getPort());try(InputStreaminputStreamsocket.getInputStream();OutputStreamoutputStreamsocket.getOutputStream()){ScannerscannernewScanner(inputStream);PrintWriterwriternewPrintWriter(outputStream,true);while(true){//1.读取请求并解析if(!scanner.hasNext()){System.out.printf([%s:%d] 客户端下线,socket.getInetAddress().toString(),socket.getPort());break;}Stringrequestscanner.next();//2.根据请求计算响应Stringresponseprocess(request);//3.把响应写回客户端writer.println(response);System.out.printf([%s:%d] req%s; resp: %s\n,socket.getInetAddress().toString(),socket.getPort(),request,response);}}catch(IOExceptione){e.printStackTrace();}}privateStringprocess(Stringrequest){returnrequest;}}2客户端UDP协议是无连接的socket是不能保存服务器IP和服务器端口号的信息的因此上篇博客在写UDP客户端时就创建了两个成员变量serveIP和servePort在构造方法中给这两个成员变量赋值构建数据包时再传入这两个成员变量TCP协议是有连接的socket对象是可以保存服务器的端口号和IP的就不需要再去创建成员变量保存IP和端口号直接在构造方法中传入端口号和IP创建socket对象即可Scanner和PrintWriter的用法前面服务器代码中已经解析过了这里就不再赘述了客户端读取用户输入通过PrintWriter把请求发送给服务器再用Scanner读取服务器的返回代码如下packagenetwork.tcp;importjava.io.IOException;importjava.io.InputStream;importjava.io.OutputStream;importjava.io.PrintWriter;importjava.net.Socket;importjava.util.Scanner;publicclassEchoClient{privateSocketsocket;publicEchoClient(StringserverIP,intserverPort)throwsIOException{socketnewSocket(serverIP,serverPort);}publicvoidstart(){System.out.println(客户端启动);ScannerscannernewScanner(System.in);try(InputStreaminputStreamsocket.getInputStream();OutputStreamoutputStreamsocket.getOutputStream()){ScannerscannerNetnewScanner(inputStream);PrintWriterwriternewPrintWriter(outputStream);while(true){//1.从控制台读取用户输入System.out.print();Stringrequestscanner.next();//2.构造请求发送给服务器writer.println(request);if(!scannerNet.hasNext()){System.out.println(服务器断开了连接);break;}//3.读取服务器响应StringresponsescannerNet.next();//4.把响应显示到控制台上System.out.println(response);}}catch(IOExceptione){e.printStackTrace();}}}3测试在服务器类中写个main方法启动服务器端口号传为9090publicstaticvoidmain(String[]args)throwsIOException{EchoServerechoServernewEchoServer(9090);echoServer.start();}上篇博客用UDP协议写了个回显服务器main方法启动时传入的端口号也是9090同时启动这两个服务器虽然端口号相同但是并不会报出异常因为TCP:9090和UDP:9090是两个完全独立的端口资源操作系统分开管理并不会有冲突因为是不同的协议UDP客户端找UDP服务器TCP客户端找TCP服务器启动这两个服务器不要终止程序可以在命令行中查一下就会发现TCP和UDP是可以同时占用9090端口号的在客户端中写个main方法启动下客户端就可以正常接收和读取数据publicstaticvoidmain(String[]args)throwsIOException{EchoClientechoClientnewEchoClient(127.0.0.1,9090);echoClient.start();}如果把客户端程序终止那服务器的scanner.hasNext()方法就会返回false服务器退出processConnection方法并且显示客户端已经下线如果客户端和服务器都启动了服务器程序终止那客户端的scannerNet.hasNext()就会返回flase显示服务器断开了连接这里用一个客户端连接服务器来测试各项功能似乎都没有问题但是这个代码中是存在两个比较严重的问题下面会进行解析2文件资源泄露问题在服务器的start方法中如果没有客户端连接这个accept方法是处于阻塞状态的每次有客户端连接循环一次就会构造一个socket对象会占用一个文件描述符服务器被客户端访问很多次进程的文件描述符耗尽之后accept()、new Socket()、getInputStream()方法都会抛出异常服务器就废掉了publicvoidstart()throwsIOException{System.out.println(服务器启动);while(true){SocketsocketserverSocket.accept();processConnection(socket);}}解决办法也很简单在processConnection方法的末尾加个finally语句里面关闭下socket对象这样每次processConnection执行完都会关闭这次连接创建的socket对象finally{try{socket.close();}catch(IOExceptione){e.printStackTrace();}}UDP服务器并不需要关闭socket资源因为代码逻辑不同UDP代码如下所示UDP服务器整个过程中只创建了一个socket对象并不是每次有客户端访问都创建一个新的socket对象packagenetwork.udp;importjava.io.IOException;importjava.net.DatagramPacket;importjava.net.DatagramSocket;importjava.net.SocketException;publicclassEchoServer{privateDatagramSocketsocket;publicEchoServer(intport)throwsSocketException{socketnewDatagramSocket(port);}publicvoidstart()throwsIOException{System.out.println(服务器启动);while(true){//1.读取请求并解析DatagramPacketreqPacketnewDatagramPacket(newbyte[4096],4096);socket.receive(reqPacket);StringrequestnewString(reqPacket.getData(),0,reqPacket.getLength());//2.根据请求计算响应Stringresponseprocess(request);//3.把响应写回到客户端DatagramPacketrespPacketnewDatagramPacket(response.getBytes(),response.getBytes().length,reqPacket.getSocketAddress());socket.send(respPacket);System.out.printf([%s:%d] req:%s, resp:%s\n,reqPacket.getAddress(),reqPacket.getPort(),request,response);}}publicStringprocess(Stringrequest){returnrequest;}publicstaticvoidmain(String[]args)throwsIOException{EchoServerservernewEchoServer(9090);server.start();}}可能有的铁汁会想TCP服务器中的scanner和writer用不用手动关闭呢并不需要因为是用try‑with‑resources的方式来写代码的pocessConnection方法执行完成后输入流和输出流会自动关闭Scanner、PrintWriter 对象只是包装对象底层流已经没了包装对象本身就失去作用就会被Java的垃圾回收机制回收3多客户端访问问题服务器基本上都是要同时服务多个客户端的前面测试TCP服务器时只测试了一个客户端访问服务器下面测试下两个客户端同时访问服务器看能不能正常的访问可以在IDEA上设置一下这样运行同一个客户端代码两次就能够启动两个客户端点击客户端右上角运行图标旁边的这个下拉框点击Edit Confjigurations编辑运行配置接着在跳出的这个页面中点击Modify options修改选项把最上面的Allow multiple instance允许多个实例打上对号再点击OK保存即可实例是一个比较广义的概念可以把创建的对象称为实例也可以把进程称为实例此时就表示可以给当前的客户端程序去启动多个进程多次运行代码可以产生多个客户端设置完成后先把服务器启动在执行两次客户端代码就能启动两个客户端如下图但是服务器只有一个客户端的上线记录而且只有第一个客户端是可以正常跟服务器交互的第二个客户端输入后没有反应如果把第一个客户端关掉这时候服务器就会显示第二个客户端的上线记录第二个客户端输入后又可以正常返回了通过测试就会发现当前的服务器处理客户端是串行处理的只有等第一个客户端程序终止才会连接第二个客户端这显然是不符合服务器要求的服务器需要能同时处理多个客户端出现这种情况就是代码的问题在服务器的start方法中写了一个while(true)循环在processConnection方法中又写了个while(true)循环当一个客户端建立连接后进入processConnection方法只要这个客户端不终止processConnection中的while(true)方法就不会退出也就是在第一个whiile(true)方法中是一直卡到processConnection(socket);并不会进入下次循环也不会尝试用accept方法获取新的客户端的连接因此新的客户段尝试连接服务器就没有反应while(true){SocketsocketserverSocket.accept();processConnection(socket);}while(true){//1.读取请求并解析if(!scanner.hasNext()){System.out.printf([%s:%d] 客户端下线,socket.getInetAddress().toString(),socket.getPort());break;}Stringrequestscanner.next();//2.根据请求计算响应Stringresponseprocess(request);//3.把响应写回客户端writer.println(response);System.out.printf([%s:%d] req%s; resp: %s\n,socket.getInetAddress().toString(),socket.getPort(),request,response);}processConnection(socket)是写到while(true)方法中的那如何做到第二个while(true)循环不会影响到第一个while(true)循环呢解决办法就是线程创建一个新的线程t去执行processConnection(socket)这样main线程执行第一个while循环t线程执行第二个while循环t线程并不会干扰main线程publicvoidstart()throwsIOException{System.out.println(服务器启动);while(true){SocketsocketserverSocket.accept();ThreadtnewThread(()-{processConnection(socket);});t.start();}}这样有客户端访问服务器时服务器就会创建个线程专门负责处理这个客户端的响应和请求代码还可以再优化下如果访问服务器的客户端非常多的话就会导致线程被频繁的创建和销毁开销比较大就可以用线程池来进行优化就创建个没有线程数量限制的线程池调用submit方法每次把任务直接交给线程池即可publicvoidstart()throwsIOException{System.out.println(服务器启动);ExecutorServicethreadPoolExecutors.newCachedThreadPool();while(true){SocketsocketserverSocket.accept();threadPool.submit(()-{processConnection(socket);});}}还存在一个问题如果有非常多的客户端访问服务器这些客户端连接服务器后并不会即可销毁像网页类服务器客户端用一会就关闭了不会连接很长时间但游戏类服务器客户端连接后普遍要保持连接的时间就比较长就会在短时间内出现大量的线程但是操作系统的线程数目是有限的解决这个问题就用到了IO复用技术IO复用技术呢可以类比宿舍带饭来理解例如博主今天要去小吃街给室友带饭博主想吃扬州炒饭、室友A想吃炸串、室友B想吃热干面那博主带饭就有三种方案先去买扬州炒饭在摊子上等老板炒完拿上扬州炒饭后再去买炸串也是在摊子上等老板炸完拿上炸串后再去买热干面等老板拌好三种饭就齐了这样等待的时间就是三种饭等待的时间之和这种就类似于一次服务器只能处理一个客户端处理完成这个客户端后才能处理下个客户端也就是串行处理的我们三个人都下楼买各等各的这样效率虽然高但是开销就比较大就类似于最后一版代码每次都用线程专门负责处理一个客户端的响应博主直接报完饭后不在摊子上等立马去下个摊子报饭接着博主站在这三个摊子的中间让老板做好了喊一下博主即可这样等待的时间就是做的最慢的那个的时间这种方法效率比较高开销也很小就是IO复用技术本质上就是用一个线程等待多个socket哪个socket有数据了线程处理就好了就能在线程有限的前提下处理更多的客户端了这个IO复用技术在C中会重点学习在Java面试中是很少涉及的Java标准库中对于多路复用的API也是比较复杂的在实际的工作中通常会用框架来封装这些底层操作因此这里铁汁们只是了解下即可结语TCP服务器的代码和需要考虑的东西是要比UDP更复杂的因为TCP 中每一个客户端连接都会对应一套独立的 Socket、输入流与输出流多个客户端同时接入时还得借助线程池实现并发处理包括writer和scanner进行写入和读取数据时初学的铁汁可能会感觉比较抽象就需要多写几次代码细品一下下篇博客开始解析网络原理部分的内容
返回列表