【Javaee】网络编程-TCP Socket
前言
前文中我们介绍了UDP Socket相关的构造方法和方法,并实现了UDP的回显服务器和客户端。
本篇将介绍TCP Socket,并使用TCP Socket api实现服务器和客户端的通信
一.TCP Socket的常见方法
1.ServerSocket
ServerSocket是创建TCP服务端Socket的API
1)ServerSocket构造方法
方法签名 | 方法说明 |
ServerSocket(int port) | 创建⼀个服务端流套接字Socket,并绑定到指定端⼝ |
2)ServerSocket方法
方法签名 | 方法说明 |
Socket accept() | 开始监听指定端⼝(创建时绑定的端⼝),有客⼾端连接后,返回⼀个服务端Socket对象,并基于该 Socket建⽴与客⼾端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
TCP建立连接的流程是操作系统内核完成的,代码感知不到,accept()操作是内核已经完成了连接建立的操作,accept()是确定对该连接进行处理(确认操作)。
如果没有客户端请求,则会阻塞。
accept会返回一个Socket对象,服务器每调用一次accept都会产生一个新的Socket对象,来和客户端进行“一对一服务”。
2.Socket
Socket 是客⼾端Socket,或是服务端中接收到客⼾端建⽴连接(accept⽅法)的请求后,返回的服务端Socket。
不管是客⼾端还是服务端Socket,都是双⽅建⽴连接以后,保存的对端信息,及⽤来与对⽅收发数据的。
1)Socket构造方法
方法签名 | 方法说明 |
Socket(String host,int Port) | 创建⼀个客⼾端流套接字Socket,并与对应IP的主机上,对应端⼝的进程建⽴连接 |
2)Socket方法
方法签名 | 方法说明 |
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输⼊流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
二.TCP服务器端实现
1.代码实现
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.Provider;
import java.util.Scanner;public class TcpServer {private ServerSocket serverSocket=null;public TcpServer(int port) throws IOException {serverSocket=new ServerSocket(port);}public void start() throws IOException {System.out.println("服务器启动");while(true){Socket client= serverSocket.accept();processConnection(client);}}private void processConnection(Socket client) {System.out.printf("[%s :%d]客户端上线",client.getInetAddress(),client.getPort());try(InputStream inputStream=client.getInputStream();//TCP是全双工通信,既能读,也能写OutputStream outputStream=client.getOutputStream();PrintWriter printWriter=new PrintWriter(outputStream)){while(true){//1.读取请求并解析(为方便读,可使用Scanner)Scanner scanner=new Scanner(inputStream);if(!scanner.hasNext()){//如果Scanner读不出下一个数据了,说明客户端关闭了连接,导致服务器读到了“末尾”break;}//2.根据请求计算响应String request=scanner.next();String response=process(request);//3.把响应写回客户端printWriter.println(response);//4.打印日志System.out.printf("[%s :%d], rep=%s,reps=%s\n",client.getInetAddress(),client.getPort(),request,response);}}catch (IOException e){e.printStackTrace();}System.out.printf("[%s :%d]服务器下线",client.getInetAddress(),client.getPort());}private String process(String response){return response;}public static void main(String[] args) throws IOException {TcpServer server=new TcpServer(9090);server.start();}
}
2.代码解析
1.建立ServerSocket类,需填入参数来绑定程序的端口号,new ServerSocket(port)后于客户端建立起连接。
2.创建start()方法启动服务器,使用accept()确认对连接客户端进行处理,将处理过程传入processConnection()函数中
3.因TCP服务器是面向字节流,因此需要使用输入流与输出流进行数据的发送与接收。(在try中定义是因为try结束后会自动释放文件资源)
4.进行数据的读取,读取完后对数据进行处理(process()中处理,因本代码为回显服务器,只是返回客户端请求,无更多逻辑),处理完后生成响应,并写回客户端,最后打印日志
注:此处服务器代码存在三个问题,需结合客户端代码进行解释与修改
三.TCP客户端实现
1.代码实现
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 TcpCilent {private Socket socket=null;public TcpCilent(String ServerIP,int Port) throws IOException {socket=new Socket(ServerIP,Port);}public void strat(){System.out.println("客户端启动");try(InputStream inputStream= socket.getInputStream();OutputStream outputStream=socket.getOutputStream();Scanner scanner=new Scanner(inputStream);PrintWriter printWriter=new PrintWriter(outputStream);Scanner scannerIn=new Scanner(System.in)){while(true){//1.从控制台读取信息String request=scannerIn.next();//2.将数据发送给服务器printWriter.println(request);//3.从服务器获取响应if(!scannerIn.hasNext()){break;}String response=scanner.next();//4.打印日志System.out.println(response);}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) throws IOException {TcpCilent cilent=new TcpCilent("127.0.0.1",9090);cilent.strat();}
}
2.代码解析
1.客户端创建Socket类,需填入服务器的ip和端口号,在new时系统内核会自与对应的服务器连接上
2.与服务器同理,因TCP面向字节流,需要通过输入流与输出流来实现数据的发送与接收。
3.从控制台中读取数据(我们输入的),然后将其写入到服务器,在获取到服务器的响应,最后打印日志。
四.问题与解决
问题一:客户端发送数据之后,并没有任何响应(缓冲区问题)
运行上述代码,服务器成功打印出客户端上线和下线
但是客户端发送数据,服务器并无响应
原因是像PrintWriter这样的类,以及很多IO流中的类,都是自带“缓冲区”的。
引入缓冲区后,进行写入操作时不会立即触发IO,而是会将数据先放入缓冲区,等到缓冲区的数据积攒一波后,在统一发送(因为频繁的IO操作开销大,这是一个优化)
解决上诉问题,只要通过flush刷新缓冲区就可解决。
问题二:服务器代码未对client进行close()操作
这里的client是“连接级别”的数据,随着客户端断开连接,这个client也就不再使用了。
即使是同一个客户端再次连接,也是一个新的client,和旧的不是同一个
因此,这里的client就应该主动关闭掉。
可以在finally关闭
问题三:尝试多个客户端来同时连接服务器
作为一个服务器,就是要同时给多个客户端进行服务的
当前代码,只能启动一个客户端,如何启动多个客户端?
要修改一些配置(如图)
此时,我们发现,我们可以启动多个客户端
但是,利用新创建的客户端向服务器发送数据,却没有响应。
若把第一个客户端关掉,服务器就会立刻针对客户端2的请求进行响应。
此时,我们的服务器只能同时给一个客户端进行服务,这是不科学的。
原因是当第一个客户端连接上服务器,服务器代码就会进入processConnection内部的while循环,此时第二个客户端无法执行到accept,使用操作积压在内核缓冲区中。
解决方法是将双重while循环改为单层while循环,我们可以利用多线程
这样,问题就得到了解决
两个客户端是不同的端口
可以共同使用服务器。
完整代码
1.客户端代码
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 TcpCilent {private Socket socket=null;public TcpCilent(String ServerIP,int Port) throws IOException {socket=new Socket(ServerIP,Port);}public void strat(){System.out.println("客户端启动");try(InputStream inputStream= socket.getInputStream();OutputStream outputStream=socket.getOutputStream();Scanner scanner=new Scanner(inputStream);PrintWriter printWriter=new PrintWriter(outputStream);Scanner scannerIn=new Scanner(System.in)){while(true){//1.从控制台读取信息String request=scannerIn.next();//2.将数据发送给服务器printWriter.println(request);printWriter.flush();//3.从服务器获取响应if(!scannerIn.hasNext()){break;}String response=scanner.next();//4.打印日志System.out.println(response);}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) throws IOException {TcpCilent cilent=new TcpCilent("127.0.0.1",9090);cilent.strat();}
}
2.服务器端代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.Provider;
import java.util.Scanner;public class TcpServer {private ServerSocket serverSocket=null;public TcpServer(int port) throws IOException {serverSocket=new ServerSocket(port);}public void start() throws IOException {System.out.println("服务器启动");while(true){Socket client= serverSocket.accept();Thread t=new Thread(()->{try {processConnection(client);} catch (IOException e) {e.printStackTrace();}});t.start();}}private void processConnection(Socket client) throws IOException {System.out.printf("[%s :%d]客户端上线\n",client.getInetAddress(),client.getPort());try(InputStream inputStream=client.getInputStream();//TCP是全双工通信,既能读,也能写OutputStream outputStream=client.getOutputStream();PrintWriter printWriter=new PrintWriter(outputStream)){while(true){//1.读取请求并解析(为方便读,可使用Scanner)Scanner scanner=new Scanner(inputStream);if(!scanner.hasNext()){//如果Scanner读不出下一个数据了,说明客户端关闭了连接,导致服务器读到了“末尾”break;}//2.根据请求计算响应String request=scanner.next();String response=process(request);//3.把响应写回客户端printWriter.println(response);printWriter.flush();//4.打印日志System.out.printf("[%s :%d], rep=%s,reps=%s\n",client.getInetAddress(),client.getPort(),request,response);}}catch (IOException e){e.printStackTrace();}finally {System.out.printf("[%s :%d]服务器下线\n",client.getInetAddress(),client.getPort());client.close();}}private String process(String response){return response;}public static void main(String[] args) throws IOException {TcpServer server=new TcpServer(9090);server.start();}
}
以上便是全部内容,如有不对,欢迎指正