网络编程套接字就是操作系统给应用程序提供的一组API(叫做socket API)。
socket 可以视为是应用层和传输层之间的通信桥梁。
传输层的核心协议有两种:TCP和UDP。
TCP:有连接;可靠传输;面向字节流;全双工;
UDP:无连接;不可靠传输;面向数据报;全双工;
有连接和无连接
有连接:像打电话,得先接通,才能交互数据
无连接:像发微信,不需要接通,直接就能发数据可靠传输与不可靠传输
可靠:传输过程中,发送方知道接收方有没有收到数据
不可靠:参考无连接,微信直接可以发消息,不知道对方有没有看见这个消息。面向字流/数据报
面向字节流:以字节为单位进行传输,类似于文件操作中的字节流。
面向数据报:以数据报为单位进行传输,一个数据报会明确大小,一次发送/接收一个完整的数据报,不能是半个数据报。全双工/半双工
全双工:一条链路,双向通信。
半双工:一条链路,单向通信。
UDP Socket 中主要涉及两个类:DatagramSocket 和 DatagramPacket.
DatagramSocket 的一个对象就对应操作系统中的一个socket文件。
DatagramPacket代表了一个UDP数据报,使用UDP传输数据的基本单位。
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建
回显:请求内容是啥,得到的响应就是啥。
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
// 3. 把响应写回到客户端。
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);
}
}
// 由于是回显服务,响应就和请求一样了
// 实际上对于一个真实的服务器来说,这个过程是复杂的,为了实现这个过程,可能需要几行甚至几万行
private String process(String request) {
return request;
}
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
@SuppressWarnings({"all"})
public class UdpEchoServer {
// 进行网络编程,第一步就需要先准备好 socket实例,这是进行网络编程的大前提
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
// 启动服务器
public void start() throws IOException {
System.out.println("启动服务器");
// UDP 不需要建立连接,直接接收从客户端来的数据即可~
while (true) {
// 1. 读取客户端发来的请求
DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);
socket.receive(requestPacket);// 为了接收数据,需要先准备一个空的DatagramPacket对象,由receive来进行填充数据
// 把 DatagramPacket 解析成一格String
String request = new String(requestPacket.getData(), 0, requestPacket.getLength(), "utf-8");
// 2. 根据请求计算响应( 由于是一个回显服务器,2省略)
String response = process(request);
// 3. 把响应写回到客户端。
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);
}
}
// 由于是回显服务,响应就和请求一样了
// 实际上对于一个真实的服务器来说,这个过程是复杂的,为了实现这个过程,可能需要几行甚至几万行
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
在客户端构造 socket 对象的时候,就不再手动指定端口号,使用无参版本的构造方法。
不指定端口号,是让操作系统自己分配一个空闲的端口号。
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
@SuppressWarnings({"all"})
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
public UdpEchoClient(String ip, int port) throws SocketException {
socket = new DatagramSocket();
serverIp = ip;
serverPort = port;
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 先从控制台读取一个用户输入的字符串
System.out.println("-> ");
String request = scanner.next();
// 2. 把这个用户输入的内容构造成一格UDP请求,并发送
// 构造的请求里包含两部分信息:
// 1) 数据的内容,request 字符串
// 2) 数据要发送给谁 服务器的IP + 端口
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIp),serverPort);
socket.send(requestPacket);
// 3. 从服务器读取响应数据,并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
socket.receive(responsePacket);
// 4. 把响应结果显示到控制台上。
String response = new String(responsePacket.getData(),0,responsePacket.getLength(),"UTF-8");
System.out.printf("req: %s, resp: %s\n",request,response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
client.start();
}
}
package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
@SuppressWarnings({"all"})
public class UdpDictServer extends UdpEchoServer{
private HashMap<String, String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
// 简单构造几个词
dict.put("cat","小猫");
dict.put("dog","小狗");
dict.put("hello","你好");
dict.put("pig","小猪");
}
@Override
public String process(String request) {
return dict.getOrDefault(request, "该词无法被翻译");
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
public class TcpDictServer extends TcpThreadPoolEchoServer{
// 定义一个字典
private Map<String, String> dict = new HashMap<>();
public TcpDictServer(int port) throws IOException {
super(port);
// 构造字典的内容
dict.put("cat", "小猫");
dict.put("dog", "小狗");
dict.put("pig", "小猪");
dict.put("haha", "哈哈");
}
@Override
public String process(String request) {
return dict.getOrDefault(request,"查无此词!");
}
public static void main(String[] args) throws IOException {
TcpDictServer server = new TcpDictServer(9090);
server.start();
}
}
在TCP API 中,也是涉及到两个核心的类,
ServerSocket(专门给TCP服务器用)
Socket(既需要给服务器用,又需要给客户端用)
前面基本都和UDP的差不多,区别就是这里的start方法,由于tcp是有连接的,不是一上来就能读数据,需要先建立连接(打电话);accept返回了一个socket对象,accept可以理解为接电话,那么接电话的前提就是有人给你打电话,如果无,那么这里accept就会阻塞。
TcpEchoServer
// listen -> 监听
// 但是在 Java socket 中是体现不出来"监听"的含义的
// 之所以这么叫,是操作系统原生的API 中有一个操作叫做listen
private ServerSocket listenSocket = null;
public TcpEchoServer(int port) throws IOException {
listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 由于 Tcp是有连接的,不能一上来就读取数据,而要先建立连接(接电话)
// accept 就是在"接电话",前提是有人给你打电话
// accept 返回了一个socket 对象,称为clientSocket,后续和客户端的沟通都是通过clientSocket来完成的
// 进一步讲,listenSocket就干了一件事,就是接电话
Socket clientSocket = listenSocket.accept();
processConnection(clientSocket);
}
}
接下来我们来写processConnection()方法的代码。
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端建立连接!", clientSocket.getInetAddress().toString(), clientSocket.getPort());
// 接下来处理请求和响应
// 这里的针对 TCP socket 的读写就和文件读写是一模一样的
try (InputStream inputStream = clientSocket.getInputStream()) {
try (OutputStream outputStream = clientSocket.getOutputStream()) {
// 循环的处理每个请求,分别返回响应
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 读取请求
if (!scanner.hasNext()) {
System.out.printf("[%s:%d] 客户端断开连接!", 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) {
throw new RuntimeException(e);
} finally {
clientSocket.close();
}
}
private String process(String request) {
return request;
}
完整代码:
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;
@SuppressWarnings({"all"})
public class TcpEchoServer {
// listen -> 监听
// 但是在 Java socket 中是体现不出来"监听"的含义的
// 之所以这么叫,是操作系统原生的API 中有一个操作叫做listen
private ServerSocket listenSocket = null;
public TcpEchoServer(int port) throws IOException {
listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 由于 Tcp是有连接的,不能一上来就读取数据,而要先建立连接(接电话)
// accept 就是在"接电话",前提是有人给你打电话
// accept 返回了一个socket 对象,称为clientSocket,后续和客户端的沟通都是通过clientSocket来完成的
// 进一步讲,listenSocket就干了一件事,就是接电话
Socket clientSocket = listenSocket.accept();
processConnection(clientSocket);
}
}
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端建立连接!", clientSocket.getInetAddress().toString(), clientSocket.getPort());
// 接下来处理请求和响应
// 这里的针对 TCP socket 的读写就和文件读写是一模一样的
try (InputStream inputStream = clientSocket.getInputStream()) {
try (OutputStream outputStream = clientSocket.getOutputStream()) {
// 循环的处理每个请求,分别返回响应
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 读取请求
if (!scanner.hasNext()) {
System.out.printf("[%s:%d] 客户端断开连接!", 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) {
throw new RuntimeException(e);
} finally {
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
TcpEchoClient
import jdk.internal.util.xml.impl.Input;
import org.omg.Messaging.SYNC_WITH_TRANSPORT;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
@SuppressWarnings({"all"})
public class TcpEchoClient {
// 用普通的 socket 即可,不用 ServerSocket了
// 此处也不用手动给客户端指定端口号,让系统自由分配
private Socket socket = null;
public TcpEchoClient(String sreverIP, int sreverPort) throws IOException {
// 其实这里是可以给的,但是这里给了之后,含义是不同的
// 这里传入的ip和端口号的含义表示的不是自己绑定,而是表示和这个ip端口建立连接口
socket = new Socket(sreverIP, sreverPort);
}
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(System.in);
String response = respScanner.next();
// 4. 把结果显示到控制台上
System.out.printf("req: %s, resp: %s\n",request, response);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}
我们可以发现这里的代码第一次accept结束之后,就会进入processConnection,在processConnection中又有一个循环,若processConnection里面的循环不停,processConnection就无法完成,就会导致外层循环无法进入下一轮,也就无法第二次调用accept了。
我们可以通过多线程的方式来解决这个问题,让主线程循环调用accept,当有客户端连接上来的时候就让主线程创建一个新线程,由新线程负责客户端的若干个请求,这个时候多个线程看上去是同时执行的。
这里我们新写一个类TcpThreadEchoServer,在原有的TcpEchoServer基础上修改以下部分代码即可。
Thread t = new Thread(()->{
processConnection(clinetSocket);
});
t.start();
TcpThreadEchoServer
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 listenSocket = null;
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 clinetSocket) {
System.out.printf("[%s:%d客户端建立连接!\n",clinetSocket.getInetAddress().toString(),clinetSocket.getPort());
try(InputStream inputStream = clinetSocket.getInputStream()){
try(OutputStream outputStream = clinetSocket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);//读取请求
while (true){
if(!scanner.hasNext()){
System.out.printf("[%s:%d]客户端断开连接!\n",clinetSocket.getInetAddress().toString(),clinetSocket.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] rep:%s,resp:%s\n",
clinetSocket.getInetAddress().toString(),clinetSocket.getPort(),request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//记得关闭操作
try {
clinetSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadEchoServer server = new TcpThreadEchoServer(9090);
server.start();
}
}
TCP 版本的线程池服务器
public class TcpThreadPoolEchoServer {
// 用于服务器的Socket
ServerSocket socket;
// 构造方法指定端口号
public TcpThreadPoolEchoServer(int port) throws IOException {
socket = new ServerSocket(port);
}
// 启动服务
public void start() throws IOException {
System.out.println("服务已启动.");
// 创建一个线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 1,
TimeUnit.SECONDS, new LinkedBlockingQueue<>());
while (true) {
// 服务器启动后就开始接收客户端连接
Socket clientSocket = socket.accept();
// 处理接收,建立连接后就把他加入到线程池里去
threadPool.submit(() -> {
processConnections(clientSocket);
});
}
}
private void processConnections(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 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把outputStream
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 {
try {
// 关闭Socket
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
// echo直接返回
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadPoolEchoServer server = new TcpThreadPoolEchoServer(9090);
server.start();
}
}
那么之前为什么UDP版本的程序就不需要多线程就可以处理多个请求呢?
因为UDP不需要连接,只需要一个循环就可以处理所有客户端的请求,但是TCP即需要处理连接,又需要处理一个连接中的多个请求。