上篇文章我们一起学习了【计算机网络】网络编程套接字之UDP数据报套接字,今天让我们来一起继续学习 网络编程套接字之TCP套接字编程
ServerSocket 是创建TCP服务端Socket的API。
方法签名 | 方法说明 |
---|---|
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
方法签名 | 方法说明 |
---|---|
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket;不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
方法签名 | 方法说明 |
---|---|
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
方法签名 | 方法说明 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
服务器启动四步骤:
看到这四个步骤,不知道有的小伙伴会不会有疑惑说,上篇文章UDP数据报套接字编程时,启动服务器只需要三个步骤啊,这怎么又多了个步骤捏❓❓❓这是因为UDP协议是无连接的,而TCP协议是有连接的,不能一上来就读取数据,而是要先建立连接,就好像我们平时打电话一样,我们要先建立连接,确保对方接通了才可以开始通话。
代码实现如下:
服务器端:
package network;
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.util.Scanner;
public class TcpEchoServer {
private ServerSocket serverSocket=null;
public TcpEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动了!");
while (true){
//由于TCP是有连接的,不能一上来就读数据,而要先建立连接(像接电话一样)
//accept就是在“接电话”,接电话的前提是,有人给你打了,如果当前没有客户端尝试建立连接,此处的accept就会阻塞
//accept返回一个socket对象,称为clientSocket,后续和客户端之间的沟通,都是通过clientSocket来完成的
//进一步讲,serverSocket就干了一件事,接电话~
Socket clientSocket=serverSocket.accept();
processConnection(clientSocket);
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d]客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//接下来处理请求和响应
//这里的针对TCP socket的读写和文件读写一模一样
try (InputStream inputStream=clientSocket.getInputStream()){
try(OutputStream outputStream= clientSocket.getOutputStream()){
//循环的处理每个请求,分别返回响应
Scanner scanner=new Scanner(inputStream);
while (true){
//1.读取请求
if (!scanner.hasNext()){
System.out.printf("[%s:%d]客户端断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
//此处用Scanner更方便,如果不用Scanner就用原生的InputStream的read也可以
String request=scanner.next();
//2.根据请求,计算响应
String response=process(request);
//3.把这个响应返回给客户端
//为了方便起见,可以使用PrintWriter把OutputStream包裹一下
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(response);
//刷新缓冲区,如果没有这个刷新,可能客户端就不能第一时间看到响应结果
printWriter.flush();
System.out.printf("[%s:%d]req:%s,resp:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
//记得关闭
//由于clientSocket是每次连接都创建个新的,也就是数目很多,并且连接断开也就不再需要了,所以它会持续的进行积累
//因此我们需要保证每次处理完的连接都要给释放了
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer=new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
客户端:
package network;
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 TcpEchoClient {
private Socket socket=null;
public TcpEchoClient(String serverIp,int serverPort) throws IOException {
//这里传入的IP和端口号的含义表示的不是自己绑定,而是表示和这个IP端口建立连接!
//调用这个构造方法,就会和服务器建立连接(打电话拨号了)
socket = new Socket(serverIp,serverPort);
}
public void start(){
System.out.println("和服务器连接成功了!");
Scanner scanner=new Scanner(System.in);
try(InputStream inputStream= socket.getInputStream()){
try(OutputStream outputStream= socket.getOutputStream()){
while (true){
//1.从控制台读取字符串
System.out.println("->");
String request=scanner.next();
//2.根据读取的字符串,构成请求,把请求发送给服务器
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
//3.从服务器读取响应,并解析
Scanner respScanner=new Scanner(inputStream);
String response=respScanner.next();
//4.把结果显示到控制台上
System.out.printf("req: %s,resp: %s\n",request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient=new TcpEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
虽然上面的TCP代码已经跑起来了,但是还存在一个很严重的问题❗❗那就是当前的服务器,在同一时刻只能处理一个连接,这就很不科学❗
那么为啥当前的服务器只能处理一个客户端嘞❓那是因为能够和客户端交互的前提是,要先调用accept,接收连接(也就是接通电话)图解如下⬇️⬇️⬇️
当前这个问题就好像,你和别人在打电话,而此时其他人若再给你打电话,就没法继续接通了。
要想解决上述问题,就得让processConnection 的执行,和前面的accept的执行互相不干扰;不能让processConnection里面的循环导致accept无法及时调用。
所以此时就需要我们之前的老朋友隆重登场了,那就是——多线程❗
那么为啥UDP版本的程序就没用多线程,也是好着的呢❓❓
因为UDP不需要处理连接,UDP只要一个循环,就可以处理所有客户端的请求;
但是此处,TCP既要处理连接,又要处理一个连接中的若干次请求,就需要两个循环,里层循环就会影响到外层循环的进度了~
因此在主线程循环调用accept 时,当有客户端连接上来的时候,就直接让主线程创建一个新线程,由新线程负责对客户端的若干个请求,提供服务。(在新线程里通过while循环来处理请求) ,这个时候多个线程是并发执行的关系(宏观上看起来同时执行)。这样的话就是各自执行各自的了,就不会相互干扰了。
注意:每个客户端连上来都需要分配一个线程
package network;
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.util.Scanner;
public class TcpThreadEchoServer {
private ServerSocket serverSocket=null;
public TcpThreadEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动了!");
while (true){
//由于TCP是有连接的,不能一上来就读数据,而要先建立连接(像接电话一样)
//accept就是在“接电话”,接电话的前提是,有人给你打了,如果当前没有客户端尝试建立连接,此处的accept就会阻塞
//accept返回一个socket对象,称为clientSocket,后续和客户端之间的沟通,都是通过clientSocket来完成的
//进一步讲,serverSocket就干了一件事,接电话~
Socket clientSocket=serverSocket.accept();
//改进方法:在这个地方,每次accept成功,都创建一个新的线程,由新线程负责执行这个processConnection方法
Thread t=new Thread(()->{
processConnection(clientSocket);
});
t.start();
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d]客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//接下来处理请求和响应
//这里的针对TCP socket的读写和文件读写一模一样
try (InputStream inputStream=clientSocket.getInputStream()){
try(OutputStream outputStream= clientSocket.getOutputStream()){
//循环的处理每个请求,分别返回响应
Scanner scanner=new Scanner(inputStream);
while (true){
//1.读取请求
if (!scanner.hasNext()){
System.out.printf("[%s:%d]客户端断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
//此处用Scanner更方便,如果不用Scanner就用原生的InputStream的read也可以
String request=scanner.next();
//2.根据请求,计算响应
String response=process(request);
//3.把这个响应返回给客户端
//为了方便起见,可以使用PrintWriter把OutputStream包裹一下
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(response);
//刷新缓冲区,如果没有这个刷新,可能客户端就不能第一时间看到响应结果
printWriter.flush();
System.out.printf("[%s:%d]req:%s,resp:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
//记得关闭
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadEchoServer tcpThreadEchoServer=new TcpThreadEchoServer(9090);
tcpThreadEchoServer.start();
}
}
客户端:
客户端代码和上述回显服务客户端代码一致
package network;
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.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpThreadPoolEchoServer {
private ServerSocket serverSocket=null;
public TcpThreadPoolEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动了!");
ExecutorService pool= Executors.newCachedThreadPool();
while (true){
//由于TCP是有连接的,不能一上来就读数据,而要先建立连接(像接电话一样)
//accept就是在“接电话”,接电话的前提是,有人给你打了,如果当前没有客户端尝试建立连接,此处的accept就会阻塞
//accept返回一个socket对象,称为clientSocket,后续和客户端之间的沟通,都是通过clientSocket来完成的
//进一步讲,serverSocket就干了一件事,接电话~
Socket clientSocket=serverSocket.accept();
//利用线程池来实现
pool.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d]客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//接下来处理请求和响应
//这里的针对TCP socket的读写和文件读写一模一样
try (InputStream inputStream=clientSocket.getInputStream()){
try(OutputStream outputStream= clientSocket.getOutputStream()){
//循环的处理每个请求,分别返回响应
Scanner scanner=new Scanner(inputStream);
while (true){
//1.读取请求
if (!scanner.hasNext()){
System.out.printf("[%s:%d]客户端断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
//此处用Scanner更方便,如果不用Scanner就用原生的InputStream的read也可以
String request=scanner.next();
//2.根据请求,计算响应
String response=process(request);
//3.把这个响应返回给客户端
//为了方便起见,可以使用PrintWriter把OutputStream包裹一下
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(response);
//刷新缓冲区,如果没有这个刷新,可能客户端就不能第一时间看到响应结果
printWriter.flush();
System.out.printf("[%s:%d]req:%s,resp:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
//记得关闭
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadPoolEchoServer tcpThreadPoolEchoServer=new TcpThreadPoolEchoServer(9090);
tcpThreadPoolEchoServer.start();
}
}