目录
在Java中,TCP流套接字是基于TCP协议实现的网络通信方式,提供面向连接、可靠、有序的双向字节流传输。
API
TCP流套接字的核心API由java.net.Socket(客户端)和java.net.ServerSocket(服务端)组成
1.Socket类(客户端)
构造函数:
·Socket()
创建一个未连接的套接字,需手动调用connect(SocketAddress endpoint)
·Socket(String host,int port)
直接连接指定主机和端口,抛出UnkownHostException或IOException
·Socket(InetAddress address,int port)
使用InetAddress对象指定服务器地址,避免DNS解析
·Socket(String host,int port,InetAddress localAddr,int localPort)
绑定本地地址和端口,用于多网卡环境或指定出口IP
核心方法:
连接管理:
·connect(SocketAddress endpoint):手动建立连接
·isConnected():检查是否已连接
·InetAddress getInetAddress():返回套接字所连接的地址
·close():关闭套接字,释放资源
·数据流获取:
·InputStream getInputStream():获取输入流(读取服务器数据)
·OutputStream getOutputStream():获取输出流(发送数据到服务器)
·参数配置:
·setSoTimeout(int timeout):设置读写超时(毫秒),超时抛出SocketTimeoutException
·setSendBufferSize(int size):设置发送缓冲区大小(默认8KB)
·setReceiveBufferSize(int size):设置接收缓冲区大小(默认8KB)
·setTcpNoDelay(boolean on):禁用Nagle算法(默认false,启用延迟发送以提高数据包效率)
·setKeepAlive(boolean on):启用TCP KeepAlive机制(默认false)
示例代码:
try (Socket socket = new Socket("example.com", 80)) {
socket.setSoTimeout(5000); // 5秒超时
socket.setTcpNoDelay(true); // 禁用Nagle算法
OutputStream out = socket.getOutputStream();
out.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".getBytes());
InputStream in = socket.getInputStream();
// 读取响应...
}
2.ServerSocket类(服务端)
构造函数
·ServerSocket(int port)
绑定指定端口,默认等待连接队列长度为50
·ServerSocket(int port,int backlog)
设置等待连接队列长度(backlog),避免客户端连接被拒绝
·ServerSocket(int port,int backlog,InetAddress bindAddr)
绑定到特定本地地址(如多网卡服务器指定监听IP)
核心方法
·监听与接受连接:
·accpet():阻塞等待客户端连接,返回Socket对象
·setReuseAddress(boolean on):允许端口在关闭后快速重用(默认false,避免BindException)
·参数配置:
·setSoTimeout(int timeout):设置accpet()方法的超时时间
·isClosed():检查是否已关闭
我们在学习完Socket和ServerSocket后,尝试写一个TCP的回显客户端/服务器
创建回显服务器-客户端
回显服务器--客户端:客户端发送什么请求,服务器便返回什么
回显服务器TcpEchoServer1
public class TcpEchoServer1 {
ServerSocket serverSocket=null;
public TcpEchoServer1(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务已启动");
while(true){
//服务器启动后 就开始接受客户端连接
Socket clientSocket=serverSocket.accept();
//处理接收
processConnections(clientSocket);
}
}
private void processConnections(Socket clientSocket) throws IOException {
//打印连接信息
System.out.printf("[%s:%d]客户端已连接\n",
clientSocket.getInetAddress().toString(),
clientSocket.getPort());
//读写数据
try(InputStream inputStream=clientSocket.getInputStream()){
try(OutputStream outputStream=clientSocket.getOutputStream()){
//用Scanner处理更方便
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 writer=new PrintWriter(outputStream);
writer.println(response);
writer.flush();
//打印日志
System.out.printf("[%s:%d]request:%s,response:%s\n",
clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//关闭Socket
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer1 server=new TcpEchoServer1(9090);
server.start();
}
}
回显客户端TcpEchoClient1
public class TcpEchoClient1 {
private Socket socket;
//构造方法指定IP和端口号
public TcpEchoClient1(String serverIp,int port) throws IOException {
socket=new Socket(serverIp,port);
}
public void start(){
System.out.println("连接服务器成功");
//用socket.getInputStream获取
try(InputStream inputStream=socket.getInputStream()){
try(OutputStream outputStream=socket.getOutputStream()){
Scanner scanner=new Scanner(System.in);
while(true){
//1.接收用户输入
System.out.println("->");
String request=scanner.next();
//2.构造数据发送到服务器
PrintWriter writer=new PrintWriter(outputStream);
//写数据
writer.println(request);
//强制刷新缓冲区,如果不刷新,客户端可能不能及时收到响应
writer.flush();
//3.接收服务器响应的数据
Scanner resScanner=new Scanner(inputStream);
String response=resScanner.next();
//4.解析响应并打印
System.out.printf("request:%s,response:%s\n",request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient1 client=new TcpEchoClient1("127.0.0.1",9090);
client.start();
}
}
服务器引入多线程
如果只是单个线程,无法同时响应多个客户端
此处给每个客户端都分配一个线程
客户端不做修改,只是在服务器处理客户端连接时,使用多线程去接收
代码如下:
public class TcpEchoServer2 {
ServerSocket serverSocket=null;
public TcpEchoServer2(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(()->{
try {
processConnections(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
});
t.start();
}
}
服务器引入线程池
为了避免频繁创建销毁线程,也可以引入线程池
这里如上,也只在服务器中进行修改。将创建多线程去接收客户端连接修改成:让线程池去提交请求,分配线程去处理客户端的请求
代码如下:
public class TcpEchoServer {
private ServerSocket serverSocket=null;
//这里和UDP服务器类似,也是在构造对象的时候,绑定端口号
public TcpEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器");
//使用线程池管理线程
ExecutorService executorService= Executors.newCachedThreadPool();
while(true){
//对于TCP来说,需要先处理客户端发来的连接
//通过读写clientSocket,和客户端进行通信
//如果没有客户端发起连接,此时accept就会阻塞
//主线程负责进行accept。每次accept到一个客户端,就创建一个线程
//由新线程负责处理客户端的请求
Socket clientSocket=serverSocket.accept();
executorService.submit(()->{
processConnection(clientSocket);
});
}
}
当大家逐一敲写代码后,可能会产生以下疑问?
解疑惑
1.服务器和客户端如何建立连接?我们这里简单讲解,后续会详细讲解(三次握手)
服务器在启动后,会阻塞等待连接(accpet方法)。
当客户端通过构造方法new Socket创建socket实例后,向服务器的serverSocket发起连接请求。
服务器接受连接,生成clientSocket(Socket clientSocket=serverSocket.accpet() )
通过服务器的clientSocket和客户端的socket进行网络通信
2.客户端从终端输入请求,服务端是如何获取请求的?
我们知道服务器和客户端分别通过clientSocket和socket进行网络通信
客户端发送请求:客户端通过PrintWriter将用户输入 写入输出流
outputStream=socket.getOutputStream();
PrintWriter writer=new PrintWriter(outputStream);
writer.println(request);//将字符串写入输出流
writer.flush();//强制刷新缓冲区,确保数据立即发送
服务端获取请求:
通过服务器的inputStream=clientSocket.getInputStream()获取客户端的输入流
然后根据
Scanner sc=new Scanner(inputStream);
String request=sc.next();
读取客户端的输入流(请求)
3.服务端根据请求返回响应,客户端又是如何获取响应的?
服务端返回响应:服务端也通过PrintWriter将响应 写入输出流
PrintWriter writer=new PrintWriter(outputStream); String response=process(request); writer.println(response); writer.flush();
客户端获取响应:
通过客户端的inputStream=socket.getInputStream()获取服务器的输入流
然后根据
Scanner scanner=new Scanner(inputstream);
String response=scanner.next()
读取服务器的输入流(响应)
4.服务器的serverSocket、clientSocket和客户端的socket三者有何联系?
服务器
serverSocket:服务器端监听套接字
clientSocket:服务器端通信套接字。clientSocket是服务器为每个连接的客户端创建的专用套接字
数据传输:
通过clientSocket.getInputStream和getOutputStream与客户端交换数据
clientSocket在客户端连接时创建,连接断开时关闭
客户端
socket:客户端套接字
·发起连接:通过new Socket(服务器IP,端口)连接到服务器
·数据传输:通过socket.getInputStream和getOutputStream与服务器通信
socket在连接服务器后创建,通信接收后关闭
三者协作关系
1.连接建立
·客户端socket向服务器的serverSocket发起连接请求
·服务器serverSocket接受连接,生成一个clientSocket
2.数据传输
·客户端通过自身socket发送数据-->服务器通过对应的clientSocket接收数据
·服务器通过clientSocket发送响应-->客户端通过自身的socket接收响应
3.连接终止
·任意一端关闭套接字,连接即断开
·clientSocket和客户端socket关闭,但serverSocket继续监听新连接
5.Scanner与流(InputStream、OutputStream)的关系
输出流(OutputStream):用于发送数据(客户端--发送请求--服务端/服务端--返回响应--客户端)
输入流(InputStream):用于接收数据(服务端接收客户端的请求/客户端接收服务端的响应)
长短连接
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
短连接:每次接收到数据并返回响应后,就关闭连接,即是短连接。也就是说,短连接只能一次收发数据
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据
对比以上 长短连接,两者区别如下:
·建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说,建立连接,关闭连接也是要耗时的,长连接效率更高
主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发
两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等
扩展了解
基于BIO(同步阻塞IO)的长连接会一直占用系统资源。对于并发要求很高的服务端系统来说,这样的消耗是不能承受的
由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行
一次阻塞等待 对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的处理请求
实时应用时,服务端一般是基于NIO(即同步非阻塞IO)来实现长连接,性能可以得到极大的提升