Socket套接字
Socket API是网络编程最核心的部分。
Socket套接字是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。
Socket API与传输层密切相关,由于传输层有UDP和TCP两种协议类型,故Socket API也提供了两种风格(准确来说有三种,Unix域套接字不常用或者说很早就没人用了)。
Socket套接字的分类
主要根据传输层协议划分为三类:
流套接字
使用TCP传输协议。
特点:
有连接
可靠传输
面向字节流 ——>数据传输文件读写类似于“流式”。
有接收缓冲区,有发送缓冲区
大小无限制
数据套接字
使用传输层UDP协议。
特点:
无连接
不可靠传输
面向数据报——>数据传输是以一个个“数据报”为基本单位(一个数据报可能是若干个字节)
有接收缓冲区,无发送缓冲区
大小有限制
原始套接字
不做了解。
此处的可靠传输和不可靠传输与有连接和无连接无关。
流套接字和数据套接字都是全双工的,可以双向传输。
UDP数据报套接字
DatagramSocket API
表示一个Socket文件。
普通的文件对应的硬件设备是硬盘,Socket文件对应的硬件设备是网卡。
DatagramSocket的构造方法
一个socket对象就可以和另一台主机进行通信。同样的,要与多台主机进行通信需要有多个socket对象。
没有指定端口,系统会给自动分配一个空闲的端口。
port指定了端口。此时就是让当前的socket对象与指定端口关联起来。
端口号一般是简单的整数
本质上,不是进程和端口建立了联系而是进程中的socket对象和端口建立了联系。
DatagramSocket的方法
此处传入的相当于是一个空的对象,receive方法内部会对参数(这个对象)进行内容的填充,从而构造出结果数据。
如果没有接收到数据报,该方法会阻塞等待。
传入的参数是下面要说的,报文。
关闭这个socket文件释放资源。
DatagramPacket API
表示UDP中传输的一个报文。
DatagramPacket是UDP Socket发送和接收的数据报,构造这个对象可以指定一些具体的数据进去。
DatagramPacket的构造方法:
其中常用的构造方法为:
接收
发送
address指定了目的主机的IP和端口号。
DatagramPacket的方法:
对于UDP来说,DatagramPacket是传输数据的基本单位。而在上述的方法中,日常使用的为以下三种方法。
方法名 | 方法说明 |
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址; 从发送的数据报中,获取接收端主机的IP地址。 |
int getPort() | 从接收的数据报中获得发送端主机的端口号; 从发送的数据报中获得接收端主机的端口号。 |
byte[] getData() | 获取数据报中的数据 |
在构造UDP发送的数据报时,需要传入socketAddress,这个对象可以用InetSocketAddress来创建。
DatagramPacket.getSocketAddress()
获取要将此包发送到的或发出此数据报的远程主机的 SocketAddress(通常为 IP 地址 + 端口号)。
InetSocketAddress API
InetSocketAddress 的构造方法
方法名 | 方法说明 |
InetSocketAddress(InetAddress addr,int port) | 创建一个socket地址,包含IP地址和端口号 |
TCP流套接字
ServerSocket API
ServerSocket的构造方法
方法名 | 方法说明 |
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定的端口上。 |
客户端在构造Socket对象的时候,会指定服务器的IP和端口。
ServerSocket方法
方法名 | 方法说明 |
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待。 |
void close() | 关闭此套接字 |
对于Socket accept()这个方法是调用一个接通一个,相当于接电话,要先建立连接才能进行通信。返回服务端Socket的前提得有客户端来建立连接,如果没有客户端来建立连接,此时这个方法就会阻塞等待。
Socket API
Socket既给客户端使用也给服务器使用。
在服务器这边,是由accept返回的。
在客户端这边,是在代码中构造出来的,构造的时候,指定一个IP和端口号(此处指定的IP和端口号是服务器的IP和端口号。)
无论是服务器还是客户端Socket都是双方建立连接之后,保存的对端信息,用来与对方收发数据的。
Socket构造方法
方法名 | 方法说明 |
Socket(String host,int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接。 |
Socket方法
方法名 | 方法说明 |
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
InputStream getInputStream()和OutputStream getOutputStream() 进一步通过Socket对象获取到内部的流对象,通过流对象进行发送/接收。
回显服务器
首先理解什么是服务器:
一般的服务器收到请求后会根据请求计算响应,并将响应返回。
而回显服务器则是省略了其中“根据请求计算响应”这个步骤,也就是说,请求是什么,返回的响应也是什么。
UDP回显服务器的实现:
UDP回显服务器客户端实现:
import javafx.scene.transform.Scale;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class EchoClient {
private String serverIp=null;
private int serverPort=0;
private DatagramSocket socket=null;
public EchoClient(String serverIp,int serverPort) throws SocketException {
socket=new DatagramSocket();
this.serverIp=serverIp;
this.serverPort=serverPort;
}
public void start() throws IOException {
System.out.println("客户端启动!");
Scanner scanner=new Scanner(System.in);
while(true){
System.out.println(">");
String request=scanner.next();
if(request.equals("exit")){
System.out.println("goodbye");
break;
}
DatagramPacket datagramPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIp),serverPort);
socket.send(datagramPacket);
DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
String response=new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
EchoClient echoClient=new EchoClient("127.0.0.1",9090);
echoClient.start();
}
}
UDP回显服务器实现:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class EchoServer {
private DatagramSocket socket=null;
public EchoServer(int port) throws SocketException {
socket=new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
String request=new String(requestPacket.getData(),0,requestPacket.getLength());
String response=process(request);
DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
System.out.printf("[%s:%d] req:%s;resp:%s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
EchoServer server=new EchoServer(9090);
server.start();
}
}
TCP回显服务器的实现:
TCP回显服务器客户端实现:
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();
OutputStream outputStream=socket.getOutputStream()) {
while(true){
System.out.print(">");
String request=scanner.next();
if(request.equals("exit")){
System.out.println("GoodBye");
break;
}
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
Scanner respScanner=new Scanner(inputStream);
String response=respScanner.next();
System.out.println(response);
}
}catch (IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client=new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}
TCP回显服务器实现:
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 TcpEchoServer {
private ServerSocket serverSocket=null;
public TcpEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器");
ExecutorService threadPool= Executors.newCachedThreadPool();
while(true){
Socket clientSocket= serverSocket.accept();
threadPool.submit(()->{
processConnection(clientSocket);
});
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d]客户端上线",clientSocket.getInetAddress().toString(),clientSocket.getPort());
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()) {
while(true){
Scanner scanner=new Scanner(inputStream);
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();
}
}
客户端和服务器:
一般对服务器的端口号要求是明确的(为了方便客户端找到服务器程序),而对客户端的端口是不指定要求的(客户端如果显式指定端口,可能会出现当前指定的端口和客户端电脑上其他程序的端口冲突这样的问题,这一问题可能会导致程序无法正确通信。
对于服务器发给客户端这个操作来说:服务器的端口就是源端口,客户端的端口是目的端口。
而对于客户端发给服务器这个操作来说:服务器的端口就是目的端口,客户端的端口号是源端口。