引言:
上篇文章我们一起学习了【计算机网络】网络编程套接字之UDP数据报套接字,今天让我们来一起继续学习 网络编程套接字之TCP套接字编程😊😊😊
TCP流套接字编程
ServerSocket API
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造方法
方法签名 | 方法说明 |
---|
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket 方法
方法签名 | 方法说明 |
---|
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 | void close() | 关闭此套接字 |
Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket;不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket 构造方法
方法签名 | 方法说明 |
---|
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
Socket 方法
方法签名 | 方法说明 |
---|
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){
Socket clientSocket=serverSocket.accept();
processConnection(clientSocket);
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d]客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
try (InputStream inputStream=clientSocket.getInputStream()){
try(OutputStream outputStream= clientSocket.getOutputStream()){
Scanner scanner=new Scanner(inputStream);
while (true){
if (!scanner.hasNext()){
System.out.printf("[%s:%d]客户端断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
String request=scanner.next();
String response=process(request);
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();
}
}
}
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 {
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){
System.out.println("->");
String request=scanner.next();
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
Scanner respScanner=new Scanner(inputStream);
String response=respScanner.next();
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){
Socket clientSocket=serverSocket.accept();
Thread t=new Thread(()->{
processConnection(clientSocket);
});
t.start();
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d]客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
try (InputStream inputStream=clientSocket.getInputStream()){
try(OutputStream outputStream= clientSocket.getOutputStream()){
Scanner scanner=new Scanner(inputStream);
while (true){
if (!scanner.hasNext()){
System.out.printf("[%s:%d]客户端断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
String request=scanner.next();
String response=process(request);
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){
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());
try (InputStream inputStream=clientSocket.getInputStream()){
try(OutputStream outputStream= clientSocket.getOutputStream()){
Scanner scanner=new Scanner(inputStream);
while (true){
if (!scanner.hasNext()){
System.out.printf("[%s:%d]客户端断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
String request=scanner.next();
String response=process(request);
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();
}
}
|