1. 什么是Socket
为了应用层和传输层能够交互,操作系统提供一些API给应用层,这些API可以把应用层的数据交给传输层,而这些API就是socket。
传输层中有很多协议,其中知名的就是TCP和UDP,因此操作系统提供了两个版本的API让应用层使用。
TCP和UDP的区别
- TCP是有连接,UDP无连接。(打电话是有连接的,要通信双方东建立好连接,才能通信;而发短信是无连接,不需要建立好连接)
- TCP是可靠传输,UDP是不可靠传输。(A给B发信息能够知道B有没有收到的是可靠传输)
- TCP是面向字节流,UDP是面向数据报。(一个以字节尾单位,一个以数据报尾单位)
- 都是全双工(类似于打电话交流,而半双工就类似于对讲机)
- 对于有可靠性要求的场景适用TCP,而对于可靠性要求不高,同时对传输效率要求很高的情况使用UDP
2. UDP的Socket编程
主要要掌握的两个类1. DatagramSocket 2. DatagramPacket
- DatagramSocket(网卡代言人)
DatagramSocket()
DatagramSocket(int port)
void receive(DatagramPacket p)
void send(DatagramPacket p)
void close()
创建UDP数据报套接字的Socket,绑定本机任意一个随机端口,一般用于客服端.
创建UDP数据报套接字的Socket,绑定本机指定端口,一般用于服务端
从此套接字接收数据报,如果没收到,就会阻塞等待
从此套接字发送数据报
关闭数据报套接字
socket本质上也是文件,socket对应网卡这个硬件设备,操作系统把网卡当作文件来管理,通过网卡发送数据,就是“写文件”,通过网卡接收数据,就是“读数据”
- DatagramPacket 数据报
DatagramPacket(byte[] buf, int length)
DatagramPacket(byte[]buf, int offset, int length,SocketAddress address)
InetAddress getAddress()
int getPort()
byte[] getData()
构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length)
构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length)。address指定目的主机的IP和端口
从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址
从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号
获取数据报中的数据
- 使用UDP实现回显服务器(复读机)
public class UDPEchoServer {
private DatagramSocket socket = null;
//服务器端要绑定端口,IP是本机,IP不用绑定
public UDPEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
//启动服务器
public void start() throws IOException {
System.out.println("服务器启动");
//服务器要随时待命
while(true) {
//1.读取请求
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);//空的数据报
//输出型参数,把请求写道requestPacket
//阻塞等待直到收到请求
socket.receive(requestPacket);
//2.根据请求计算响应
//把requestPacket转成字符串,方便打印
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
String response = process(request);
//3.把响应写回客服端,数据报要有客服端的地址
//requestPacket.getSocketAddress() 客户端的地址IP,端口
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), 0, 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 {
UDPEchoServer server = new UDPEchoServer(8080);
server.start();
}
}
public class UdpEchoClient {
private DatagramSocket socket = null;
//服务器的IP,端口先存起来,后面要用
private String serverIp;
private int serverPort;
//客服端不需要手动指定端口,因为你不知道客服端哪些端口是开发的,哪些是被占用的
//让操作系统自动分配一个空闲端口,客服端的IP是本机IP不用绑定
public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
socket = new DatagramSocket();
//等会要转成是点分十进制,如127.0.0.1
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while(true) {
//用户输入内容
System.out.println("->");
String request = scanner.next();
//构造一个UDP请求,包含内容,目的IP,目的端口
//InetAddress.getByName(this.serverIp)把字符串IP转成点分十进制
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(this.serverIp), this.serverPort);
socket.send(requestPacket);
//3.从服务器读取UDP响应
//空数据报
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
//阻塞等待,直到收到响应
socket.receive(responsePacket);//输出型参数,把响应写到responsePacket
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
//把响应写到控制台
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 8080);
client.start();
}
}
流程图如下:
如何在idea中启动多个客户端
咱们能够实现多个客服端访问一个服务器
3.Tcp的Socket编程
- ServerSocket类(给服务端使用)
构造方法
ServerSocket(int port)
创建一个服务端流套接字Socket,绑定端口
普通方法
Socket accept()
接收客服端连接,返回一个服务端Socket,并阻塞等待
void close()
关闭socket
- Socket类(既给服务器用,又给客服端用)
构造方法
Socket(String host, int port)
创建一个客服端的Socket,并与对应的ip,端口的进程建立连接
普通方法
InetAddress getInetAddress()
返回对方的IP,端口
InputStream getInputStream()
OutputStream getOutStream()
返回此套接字的输入流
返回此套接字的输出流
- 实现TCP版本的回显服务器
public class TcpEchoServer {
private ServerSocket listenSocket = null;
//服务器端绑定端口
public TcpEchoServer(int port) throws IOException {
listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
//使用多线程,让多个服务端都能访问
//使用线程池
ExecutorService service = Executors.newCachedThreadPool();
while(true) {
//1. 使用accept()接受客服端的连接
Socket clientSocket = listenSocket.accept();
//2.处理客服端的连接,使用多线程,每个客服端连上来都分配一个新的线程负责处理
service.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客服端上线\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort());
//处理客服端请求
//放到try里面,结束时自动关闭输入,输出流
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
while(true) {
//1.读取请求并解析
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNext()) {
//读完了,断开连接
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.将响应写回客服端
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 {
//一个客服端就创建一个clientSocket,要关闭防止文件资源泄露
//而listenSocket是全局只有一个,不用关闭,随着进制结束而结束
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(6060);
server.start();
}
}
如何理解listenSocket和clientSocket
listenSocket通过accept方法和客服端建立连接,一个clientSocket处理一个客服端连接。以卖房子的中介为例,listenSocket就类似外场销售,负责在外面拉客(可以拉很多客人),而clientSocket就是内场销售,一个内场销售服务一个客人。
为什么需要关闭clientSocket的连接
clientSocket在循环中,每个客服端连接上来就要分配一个,这个对象就会被反复的创建出实例,每创建一个,都要消耗一个文件描述符,,因此需要及时释放防止文件资源泄露
为什么需要多线程,而UDP版本的不需要
当没有客服端连接时,服务器就会阻塞到accept,如果第一个客服端过来,此时就会进入processConnection方法,连接建立好了以后,但是客服端还没发消息,此时代码阻塞在hasNext。当第二个客服过来,无法调用accept,也就无法处理第二个客服端,所以需要多线程。
UDP是无连接的,客服端直接请求即可,不必专注处理某个客服端。而TCP建立连接后,要处理客服端多次请求,才导致无法快速调用到accept方法,这是长连接。但改成短连接时TCP就能快速调用到accept方法。
public class TcpEchoClient {
//客服端使用Socket对象和服务端建立连接
private Socket socket = null;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
//和服务端建立连接需要知道服务端的IP,Port
socket = new Socket(serverIp, serverPort);
}
public void start() {
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while(true) {
//1.从控制台读取数据,构造请求
System.out.print("->");
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.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 6060);
client.start();
}
}