目录
TCP客户端常见的API:
ServerSocket:
Socket:
TCP服务端(单线程版本)
属性+构造方法:
启动服务端的start()方法
步骤一:接收客户端发送的socket
步骤二: 调用processConnection方法来处理客户端发送的连接
①通过参数传入的clientSocket来获取输入、输出流对象,此处采用的是try()的方法来关闭流对象
②通过scanner.next()来不断读取内容;
③构造response,来作出响应
④通过OutputStream+PrintWriter来发送response字符串给客户端
⑤关闭连接
TCP客户端(单线程版本)
属性+构造方法
start方法
步骤1:通过socket来获取到与服务端进行数据交互的inputStream和OutputStream
步骤2:从控制台获取用户输入的信息
步骤3:把读取到的request以流的形式发送给服务端,获取响应
步骤4:通过Scanner读取服务器的响应,并且回显
TCP服务端(支持多个客户端发送请求)
多线程版本服务端
线程池版本服务端
TCP长连接/短连接问题
短连接的工作过程:
长连接的工作过程:
TCP协议的具体介绍,已经在上一篇文章当中提到了。
同时,上一篇文章也手写了一个Udp协议。
(2条消息) 认识UDP、TCP协议_革凡成圣211的博客-CSDN博客https://blog.csdn.net/weixin_56738054/article/details/128709206?spm=1001.2014.3001.5501 在这一篇文章当中,udp的客户端和服务端之间的通信,使用的时DatagramSocket和DatagramPacket这两个api来完成传递信息的。DatagramSocke负责发送和接收消息,DatagramPacket用来传输报文
而在TCP协议当中,提供的API主要是下面两个类:
ServerSocket:专门给服务端使用的socket。
Socket:既可以提供给客户端使用,也可以给服务端使用。
构造方法:
方法签名 | 方法说明 |
ServerSocket(int port) | 创建一个服务端嵌套字,并且指定服务端所占用的进程 |
成员方法: accept
方法签名 | 方法说明 |
Socket accept() | accept方法,用来表示建立客户端与服务端的连接 前面一篇文章当中,我们提到了,TCP是"有连接"的协议,TCP客户端与服务端一定要建立连接,才可以互相发送消息。因此这个accept方法,返回的socket对象,服务端就是通过这个socket对象和客户端进行通信的。 如果服务端没有收到socket对象,那么就会阻塞等待,无法进行通信。 |
对于服务端来说,是由accept()方法返回的的,返回的socket对象用于和客户端进行通信。
构造方法
对于客户端来说,在客户端的构造方法当中,需要构造对象的时候,指定一个IP以及端口号
这个IP以及端口号都是服务端的
两个常用普通方法
方法签名 | 方法说明 |
getInputStream() | 通过socket对象,获取到内部的输入流对象 |
getOutputStream() | 通过socket对象,获取到内部的输出流对象 |
需要在TcpEchoServer内部封装一个属性,这个属性是ServerSocket。
在构造方法当中,需要指定ServerSocket占用哪个端口号,此端口号就是服务端的端口号
代码实现:
/**
* @author 25043
*/
public class TcpEchoServer {
/**
* 用于Tcp客户端与服务端通信
* 的socket对象
*/
private ServerSocket serverSocket;
public TcpEchoServer(int port) throws IOException {
//指定服务端进程占用的端口号
serverSocket=new ServerSocket(port);
}
//使用clientSocket来与客户端进行交流
Socket clientSocket=serverSocket.accept();
此处serverSocket.accept()方法的效果是接收客户端发送的连接。
一个客户端对应一个accept方法获取的clientSocket
由于Socket代表一个文件,任何一个文件会对应进程当中的一个文件描述符表。
也就是这个socket会占用额外的磁盘空间,因此当客户端和服务端通信结束之后,需要把这个连接释放掉(释放的操作,会在后面提到)
客户端在构造socket对象的时候,就会指定服务端的IP以及端口号。
客户端如果想与服务端通信,一定需要建立连接!!因此,如果发服务端启动之后,没有客户端发送连接过来,那么服务端就会在accept()方法这里阻塞等待。
因此,在服务端当中,通信的逻辑应当是这样的:
需要注意的是:一个Socket对应的是一个客户端发送的连接,但是在processConnection内部额有可能涉及处理多个客户端连接的步骤:也就是在Tcp协议当中,服务端与客户端的关联关系为一对多。
但是,以下的代码,先来体验一下单线程的模式。最后,将会演示一个多线程版本。
//获取clientSocket当中输入、输出流对象
try(InputStream inputStream= clientSocket.getInputStream();
OutputStream outputStream= clientSocket.getOutputStream()) {
以下②③④一共3个步骤,需要在while(true)循环内部不断进行,直到scanner无法读取到内容了
//2、根据请求构造响应
//通过scanner.next()的方式来读取,需要注意的是
//scanner遇到空格/换行符/其他空白字符会停止读取
//但是,读取的结果里面不会包含这三种符号
String request=scanner.next();
//回写的内容
String response="服务端已经响应:"+request;
//使用PrintWriter来发送outputStream
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(response);
//刷新缓冲区,保证当前数据一定会被发送出去
printWriter.flush();
在finally代码块当中,关闭连接,释放文件描述符表。
finally {
try {
//关闭此次连接
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
整体服务端代码(单线程版)
/**
* @author 25043
*/
public class TcpEchoServer {
/**
* 用于Tcp客户端与服务端通信
* 的socket对象
*/
private ServerSocket serverSocket;
public TcpEchoServer(int port) throws IOException {
//指定服务端进程占用的端口号
serverSocket=new ServerSocket(port);
}
/**
* 启动服务端
*/
public void start() throws IOException {
System.out.println("启动服务端");
while (true){
//使用clientSocket来与客户端进行交流
Socket clientSocket=serverSocket.accept();
processConnection(clientSocket);
}
}
/**
* 处理客户端发送的连接
* 客户端发送的连接@param clientSocket
*/
private void processConnection(Socket clientSocket) {
//输出客户端的IP以及端口号
System.out.println("客户端已经上线!客户端的IP是:"
+clientSocket.getInetAddress()+
";客户端的端口是:"+clientSocket.getPort());
//获取clientSocket当中输入、输出流对象
try(InputStream inputStream= clientSocket.getInputStream();
OutputStream outputStream= clientSocket.getOutputStream()) {
//使用while循环,处理多个请求+响应
while (true){
//1、通过scanner来读取inputStream
Scanner scanner=new Scanner(inputStream);
//读取完毕之后,直接返回:
if(!scanner.hasNext()){
System.out.println("客户端已经下线!客户端的IP是:"
+clientSocket.getInetAddress()+
";客户端的端口是:"+clientSocket.getPort());
//退出循环
break;
}
//2、根据请求构造响应
//通过scanner.next()的方式来读取,需要注意的是
//scanner遇到空格/换行符/其他空白字符会停止读取
//但是,读取的结果里面不会包含这三种符号
String request=scanner.next();
//构造回写的内容response
String response="服务端已经响应:"+request;
//使用PrintWriter来发送outputStream
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(response);
//刷新缓冲区,保证当前数据一定会被发送出去
printWriter.flush();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
此处,需要一个socket,指定服务端的IP+端口号
private Socket socket;
public TcpEchoClient(String serverIp,int port) throws IOException {
//指定服务端的ip+端口号
socket=new Socket(serverIp,port);
}
如果客户端想和服务端进行通信,就一定需要指定服务端的端口号。因为TCP是有连接的协议,不允许在没有建立连接的情况下面发送消息。
当socket对象被创建之后,也就意味着客户端成功与服务端建立连接。
客户都安的socket创建之后的一瞬间,服务端的accept方法已经接收早到就客户端的socket对象。
需要注意的是,从客户端的socket获取的InputStream和OutputStream都是相对于客户端来进行输入/输出操作的。
//1.客户端从键盘上面读取内容
String request=input.next();
//2.把读取到的内容构造成请求,发送到客户端
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(request);
//加上flush,刷新缓冲区
printWriter.flush();
可以看到,此处,使用的是printWriter.println(request)来发送字符串的
但是,是否可以替换成print,也就是不采用\n呢?
答案是,不可以:原因:
在服务端当中,是使用Scanner scanner=input.next()来接收客户端发送的内容的:
回顾一下scanner.next()在什么时候会停止读取,那就是在读取到\n或者空格或者空白字符的时候,就会停止读取。因此,此处客户端发送的内容当中,一定要带有\n,才可以确scanner.next()停止读取。
//读取服务器响应
Scanner scanner=new Scanner(inputStream);
String response= scanner.next();
//把响应的内容回显到界面上面
System.out.println(response);
整体客户端代码:
/**
* Tcp客户端
* @author 25043
*/
public class TcpEchoClient {
private Socket socket;
public TcpEchoClient(String serverIp,int port) throws IOException {
//指定服务端的ip+端口号
System.out.println("服务端已经指定端口号"+System.currentTimeMillis());
socket=new Socket(serverIp,port);
}
public void start(){
System.out.println("客户端启动!");
Scanner input=new Scanner(System.in);
//此处获取到的输入流、输出流对象,都是已经跟客户端建立了联系的
try (InputStream inputStream= socket.getInputStream();
OutputStream outputStream= socket.getOutputStream()){
while (true){
//1.客户端从键盘上面读取内容
String request=input.next();
//2.把读取到的内容构造成请求,发送到客户端
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(request);
//加上flush
printWriter.flush();
//读取服务器响应
Scanner scanner=new Scanner(inputStream);
String response= scanner.next();
//把响应的内容回显到界面上面
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
//指定服务端的ip以及端口号
TcpEchoClient tcpEchoClient=new TcpEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
单线程TCP存在问题分析:
TCP的服务端的核心代码就是start方法
当服务端启动之后,如果有客户端与服务端建立联系,那么accept方法就会返回一个socket对象,服务端使用这个socket对象与客户端进行通信。
紧接着,服务端在processConnection方法当中,针对客户端发送过来的clientSocket进行不断地使用while循环,调用scanner.next方法进行读取操作。
那么,也就意味着,只要客户端不下线,服务端就会一直停留在这个processConnection的while循环当中。
由于在上述的代码当中,服务端的代码是单线程的。因此,服务端无法从processConnection方法当中离开,即使其他的客户端想再次给服务端建立连接,服务端也accept不到。
但是,如果在processConnection当中不采用while循环,那么这样可以吗?
也是不行的,原因:
如果不使用while循环,那么,服务端只会读取一次客户端发送的请求,也就是调用一次scanner.next方法,然后服务端就会把连接给close掉了。
那么这个客户端如果想再次建立连接,就需要重新获取连接,也就是再次new一个Socket对象。
但是,在上面客户端的代码当中,调用客户端构造方法的时候,只创建了一个连接,也就是一个socket。
因此,如果客户端想要再次发送消息,就没有办法发送了。
客户端代码:
大部分的代码写法都和单线程的一致,唯一的区别就在于,每调用一次processConnection方法需要创建新一个线程来执行。
这样,每获取到一个clientSocket,就会创建一个新的线程t来执行processConnection方法。
即使线程t出现了异常情况,无法结束运行,也不会影响主线程不断接收新的客户端连接。
代码实现:
/**
* 启动服务端(多线程版)
*/
public void start() throws IOException {
System.out.println("启动服务端");
while (true){
//使用clientSocket来与客户端进行交流
Socket clientSocket=serverSocket.accept();
Thread t=new Thread(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
t.start();
}
}
以上代码,在客户端数量不大的情况下面,是可以行得通的
但是,如果客户端数量比较庞大,并且线程的创建、销毁工作也是开销比较大的,因此,可以考虑使用线程池来处理processConnection方法,这样就可以减少了线程不断创建、销毁带来的开销。
代码实现:
private ExecutorService threadPool= Executors.newCachedThreadPool();
/**
* 启动服务端(多线程版)
*/
public void start() throws IOException {
System.out.println("启动服务端");
while (true){
//使用clientSocket来与客户端进行交流
Socket clientSocket=serverSocket.accept();
//往线程池当中提交任务
threadPool.submit(() -> processConnection(clientSocket));
}
}
①客户端与服务器建立连接
②发送一次请求
③读取响应
④关闭连接
下次通信,就需要再一次建立连接。可以看到,短连接每一次通信只会建立一次连接。
①客户端与服务端建立连接
②客户端发送消息
③读取响应
④根据需求,尝试再次发送消息(也就是回到2)
⑤重复②到④之间若干次,再决定是否断开连接
可以看到,长连接的特点就是一次连接多次发送消息。而短连接,就是一次连接只可以发送一次请求。看似长连接的复用性更高,但是其实也不一定说要使用长连接的策略才好,需要结合具体的应用场景。