
目录
网路编程就是通过代码来控制,让两个主机的进程之间能够进行数据交互。操作系统把网络编程的一些相关操作,封装起来了,提供了一组API(接口)来调用。
1、流套接字(底层使用TCP协议)
2、数据报套接字(底层使用UDP协议)
TCP特点:
1、有连接
2、可靠传输
3、面向字节流
4、全双工
UDP特点:
1、无连接
2、不可靠传输
3、面向数据报
4、全双工
有连接就相当于我们打电话,A打给B,B接起来了,这时候A和B才能进行通信。
无连接就相当于我们发信息,A发过去给B了,B收到了,但是我们也不能确定B是否有看到消息。
可靠传输就是说发送方能够知道对方收到了,不可靠传输就是说发送方不知道对方是不是收到了。
面向字节流:我们为了发送100个字节,可以一次发送1个字节,重复100次,也可以一次发送10个字节,重复10次……非常灵活完成发送,接收也是如此
面向数据报:以一个一个数据报为基本单位(每个数据报多大,不同协议约定的不一样),发送的时候一次至少发送一个数据报(如果尝试一次发一个半,实际只能发出去一个),接收的时候一次至少接收一个数据报(如果尝试接收半个,剩下半个就没有了)
全双工:双向通信表示A和B可以同时向对方发送接收数据;半双工:单向通信,要么A给B发,要么B给A发,不能同时发。
UDP Socket有两个核心的类:
1、DatagramSocket 描述一个Socket对象。
receive:面向数据报,接收数据,如果没有数据过来,receive就会阻塞等待,如果有数据,receive就能返回一个DatagramPacket对象。
send:发送数据,以DatagramPacket为单位进行发送
2、DatagramPacket 描述一个UDP数据报。
例子1:基于UDP的socket来写一个比较简单的回显(Echo)程序回显客户端+回显服务器,回显就是A说啥B就回应啥。
正常的客户端/服务器通信流程:

代码示例:
UdpEchoServer 服务器
- //服务器
- public class UdpEchoServer {
- private DatagramSocket socket=null;
-
- //port表示端口号
- //服务器在启动的时候,需要关联上一个端口号
- //收到数据的时候,就会根据这个端口号来决定把这个数据交给哪个进程
- //虽然这里的port写的是int类型,但是实际上端口号是一个两个字节的无符号整数
- //范围是0-65535
- public UdpEchoServer(int port) throws SocketException{
- socket=new DatagramSocket(port);
- }
- //通过这个方法启动服务器
- public void start() throws IOException {
- System.out.println("服务器启动");
- //服务器一般都是持续运行的(7*24)
- while (true) {
- //1、读取请求
- //由于当前服务器不知道客户端什么时候发来请求,所以receive方法会阻塞
- //如果真的有请求过来了,此时receive就会返回
- //参数DatagramPacket 是一个输出型参数,socket中读取到的数据会设置到这个参数的对象中
- //DatagramPacket在构造的时候,需要一个指定的缓冲区(就是一段内存空间,通常使用byte[])
- DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
- //receive得到的结果并没有作为一个返回值,而是一个输出型参数(这个传入的参数未知,需要通过receive这个函数传出来)
- //这样子可以让用户给DatagramPacket制定一个缓冲区
- socket.receive(requestPacket);
- //把requestPacket对象的内容取出来,作为一个字符串
- String request=new String(requestPacket.getData(),0,requestPacket.getLength());
-
- //2、根据请求计算响应
- String response=process(request);
-
- //3、把响应写回客户端
- DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,
- requestPacket.getSocketAddress());
- //此处DatagramPacket中设置的长度,必须是"字节的个数"
- //如果直接取response.length()得到的就是字符串长度 也就是"字符个数"
- // responsePacket在构造的时候,还得指定这个包要发给谁,发送的目标就是发送请求的那一方
- socket.send(responsePacket);
-
- //4、打印
- String log=String.format("[%s:%d] req: %s; resp :%s",
- requestPacket.getAddress().toString(),//IP地址
- requestPacket.getPort(),//端口号
- request,response,"hello");
- System.out.println(log);
-
- }
- }
-
- //根据请求计算响应
- //由于此处是回显服务器,所以直接把客户端请求返回即可
- private String process(String request) {
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- UdpEchoServer server=new UdpEchoServer(9090);
- server.start();
- }
- }
UdpEchoClient 客户端
- public class UdpEchoClient {
- private DatagramSocket socket=null;
- private String serverIp;
- private int serverPort;
-
- public UdpEchoClient(String serverIp,int serverPort) throws SocketException{
- this.serverIp=serverIp;
- this.serverPort=serverPort;
- //DatagramSocket实例化这里没有传入port,相当于没有指定端口号,系统会自动分配一个空闲的端口号给客户端
- this.socket=new DatagramSocket();
- }
- public void start() throws IOException{
- Scanner scanner=new Scanner(System.in);
- while(true){
- //1、从标准输入读取一个数据
- System.out.println("->");
- String request=scanner.nextLine();
- if(request.equals("exit")){
- System.out.println("exit!");
- return;
- }
- //2、把字符串构造成一个UDP请求,并发送数据
- //这个DatagramPacket,既要包含具体的数据,又要包含这个数据发给谁
- DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
- InetAddress.getByName(serverIp),serverPort);
- socket.send(requestPacket);
- //3、尝试从服务器读取响应
- DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
- socket.receive(responsePacket);
- String response=new String(responsePacket.getData(),0,responsePacket.getLength());
- //4、显示这个结果
- String log=String.format("req: %s; resp: %s",request,response);
- System.out.println(log);
-
- }
- }
-
- public static void main(String[] args) throws IOException{
- UdpEchoClient client=new UdpEchoClient("127.0.0.1", 9090);
- client.start();
- }
- }
例子2:基于UDP的socket的翻译程序
代码示例:
服务器
- public class UdpEchoServer {
- private DatagramSocket socket=null;
-
- private HashMap
dict=new HashMap<>(); -
-
- public UdpEchoServer(int port) throws SocketException {
- socket=new DatagramSocket(port);
-
- //对hash表的值进行初始化
- //根据需求,这个表的数据可以非常多
- dict.put("hello","你好");
- dict.put("fruit","水果");
- dict.put("animal","动物");
- }
- private String process(String request) {
- //利用查表来实现翻译的效果
- return dict.getOrDefault(request,"[单词在词典中不存在]");
- }
- public void start() throws IOException {
- System.out.println("服务器启动");
- while(true){
- //1、读取请求并解析
- DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
- socket.receive(requestPacket);
- String request=new String(requestPacket.getData(),0,requestPacket.getLength());
- //2、根据请求计算响应
- String response=process(request);
- //3、把响应写回客户端
- DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,
- requestPacket.getSocketAddress());
- socket.send(responsePacket);
- //4、打印
- String log=String.format("[%s:%d] req: %s; resp: %s",
- requestPacket.getAddress().toString(),
- requestPacket.getPort(),
- request,response);
- System.out.println(log);
-
- }
- }
-
-
-
- public static void main(String[] args) throws IOException {
- UdpEchoServer server=new UdpEchoServer(9090);
- server.start();
- }
- }
客户端
- public class UdpEchoClient {
- private DatagramSocket socket=null;
- private String serverIp;
- private int serverPort;
-
- public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
- this.serverIp=serverIp;
- this.serverPort=serverPort;
- socket=new DatagramSocket();
- }
-
- public void start() throws IOException {
- Scanner sc=new Scanner(System.in);
- while(true){
- //1、读取数据
- System.out.println("->");
- String request=sc.nextLine();
- if(request.equals("exit")){
- System.out.println("exit!");
- return;
- }
- //2、构造请求并发送给服务器
- DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
- InetAddress.getByName(serverIp), serverPort);
- socket.send(requestPacket);
- //3、从服务器中读取响应
- DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
- socket.receive(responsePacket);
- String response=new String(responsePacket.getData(),0,responsePacket.getLength());
- //4、打印
- String log=String.format("req: %s; resp: %s",request,response);
- System.out.println(log);
-
- }
- }
-
- public static void main(String[] args) throws IOException{
- UdpEchoClient client=new UdpEchoClient("127.0.0.1", 9090);
- client.start();
- }
- }

TCP socket这里用的类是ServerSocket和Socket,这里有个accept特别要注意:
accept:和TCP中的"有连接"这个特性密切相关,如果A给B打电话,B要接通电话,A和B才能通信,accept就相当于接电话这个动作,客户端尝试建立连接,首先服务器先建立好一切连接,做好准备工作,客户端调用accept,才是真的把这个连接拿到用户代码中。
注意:
socket对应到文件了,所以也会有一个close方法,打开一个文件之后,要记得及时关闭!如果不关闭就会出现资源泄露。但是前面写的Udp版本我们没有用close关闭,其实是因为Udp版本的程序不太需要关闭(也不能关闭),UdpServer、UdpClient里面的socket生命周期都是需要跟随整个程序的。如果socket没有关闭,当进程结束的时候,对应的资源也就自然释放了,资源描述的是文件描述符,文件描述符在文件描述符表里面,文件描述符表在PCB里,如果进程没了,对于的PCB就已经销毁了。
例子:用 TCP socket 写一个回显程序
代码示例:
服务器
- 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("程序启动");
- while(true){
- //UDP的服务器进入主循环,就直接尝试receive请求
- //但是TCP是有连接的,首先需要做的是建立好连接
- //当服务器运行的时候,如果客户端没有建立连接,accept就会阻塞等待
- //如果客户端建立连接了,accept就会返回一个socket对象
- //进一步的服务器和客户端之间的交互,就交给clientSocket来完成了
- Socket clientSocket=listenSocket.accept();
- processConnection(clientSocket);
- }
- }
-
- private void processConnection(Socket clientSocket) throws IOException {
- String log=String.format("[%s:%d] 客户端上线!",
- clientSocket.getInetAddress().toString(),
- clientSocket.getPort());
- System.out.println(log);
- //TCP socket也是字节流,进行数据读写时跟之前读写文件是类似的,也是使用InputStream和OutputStream
- try(InputStream inputStream=clientSocket.getInputStream();
- OutputStream outputStream= clientSocket.getOutputStream()){
- while(true){
- //1、读取请求并解析
- Scanner sc=new Scanner(inputStream);
- if(!sc.hasNext()) {
- log = String.format("[%s:%d] 客户端下线!",
- clientSocket.getInetAddress().toString(),
- clientSocket.getPort());
- System.out.println(log);
- break;
- }
- String request=sc.next();
-
- //2、根据请求计算响应
- String response=process(request);
-
- //3、把响应写回客户端
- PrintWriter writer=new PrintWriter(outputStream);
- writer.println(response);
- writer.flush();
-
- log=String.format("[%s:%d] req: %s; resp: %s",
- clientSocket.getInetAddress().toString(),clientSocket.getPort(),
- request,response);
- System.out.println(log);
- }
- }catch(IOException e){
- e.printStackTrace();
- }finally {
- //当前clientSocket的生命周期并不是跟随整个程序的,而是和连接有关
- //所以每个连接结束的时候都需要进行关闭
- clientSocket.close();
- }
- }
-
- private String process(String request) {
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- TcpEchoServer server=new TcpEchoServer(9090);
- server.start();
- }
- }
客户端
- 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("程序启动");
- while(true){
- //UDP的服务器进入主循环,就直接尝试receive请求
- //但是TCP是有连接的,首先需要做的是建立好连接
- //当服务器运行的时候,如果客户端没有建立连接,accept就会阻塞等待
- //如果客户端建立连接了,accept就会返回一个socket对象
- //进一步的服务器和客户端之间的交互,就交给clientSocket来完成了
- Socket clientSocket=listenSocket.accept();
- processConnection(clientSocket);
- }
- }
-
- private void processConnection(Socket clientSocket) throws IOException {
- String log=String.format("[%s:%d] 客户端上线!",
- clientSocket.getInetAddress().toString(),
- clientSocket.getPort());
- System.out.println(log);
- //TCP socket也是字节流,进行数据读写时跟之前读写文件是类似的,也是使用InputStream和OutputStream
- try(InputStream inputStream=clientSocket.getInputStream();
- OutputStream outputStream= clientSocket.getOutputStream()){
- while(true){
- //1、读取请求并解析
- Scanner sc=new Scanner(inputStream);
- if(!sc.hasNext()) {
- log = String.format("[%s:%d] 客户端下线!",
- clientSocket.getInetAddress().toString(),
- clientSocket.getPort());
- System.out.println(log);
- break;
- }
- String request=sc.next();
-
- //2、根据请求计算响应
- String response=process(request);
-
- //3、把响应写回客户端
- PrintWriter writer=new PrintWriter(outputStream);
- writer.println(response);
- writer.flush();
-
- log=String.format("[%s:%d] req: %s; resp: %s",
- clientSocket.getInetAddress().toString(),clientSocket.getPort(),
- request,response);
- System.out.println(log);
- }
- }catch(IOException e){
- e.printStackTrace();
- }finally {
- //当前clientSocket的生命周期并不是跟随整个程序的,而是和连接有关
- //所以每个连接结束的时候都需要进行关闭
- clientSocket.close();
- }
- }
-
- private String process(String request) {
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- TcpEchoServer server=new TcpEchoServer(9090);
- server.start();
- }
- }

以上就是一个简单的基于TCP Socket 的回显程序,但是还存在问题,在实际开发中,一个服务器可以对应很多个客户端,但是在这个程序中我们可以发现这个服务器只能对应一个客户端,客户端1下线之后,客户端2才上线,不符合逻辑,所以我们需要使用多线程来处理,使得一个服务器可以对应多个客户端。

多线程处理的程序:
因为根据程序我们可以知道是服务器这里的逻辑有些问题,当没有客户端建立连接的时候,此时服务器就阻塞在accept方法上,当有客户端连接的时候,accept返回得到clientSocket并且进入processConnection方法,此时在这个方法中,代码就在sc.next()请求这里阻塞,如果有请求,结束阻塞读取数据,处理完请求之后又回到hasNext()这里阻塞,相当于说客户端什么时候退出,循环啥时候结束。此时我们需要使用多线程,让主线程里面循环调用accept,每次获取到一个连接,就创建一个线程,让这个线程来处理这个连接。
代码示例:
- public class TcpThreadEchoServer {
- private ServerSocket listenSocket=null;
-
- public TcpThreadEchoServer(int port) throws IOException {
- listenSocket=new ServerSocket(port);
- }
-
- public void start() throws IOException {
- System.out.println("服务器启动");
- while(true){
- Socket clientSocket=listenSocket.accept();
- Thread t=new Thread(){
- @Override
- public void run() {
- try {
- processConnection(clientSocket);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- };
- t.start();
- }
- }
-
- private void processConnection(Socket clientSocket) throws IOException {
- String log=String.format("[%s:%d] 客户端上线!",
- clientSocket.getInetAddress().toString(),
- clientSocket.getPort());
- System.out.println(log);
- //TCP socket也是字节流,进行数据读写时跟之前读写文件是类似的,也是使用InputStream和OutputStream
- try(InputStream inputStream=clientSocket.getInputStream();
- OutputStream outputStream= clientSocket.getOutputStream()){
- while(true){
- //1、读取请求并解析
- Scanner sc=new Scanner(inputStream);
- if(!sc.hasNext()) {
- log = String.format("[%s:%d] 客户端下线!",
- clientSocket.getInetAddress().toString(),
- clientSocket.getPort());
- System.out.println(log);
- break;
- }
- String request=sc.next();
-
- //2、根据请求计算响应
- String response=process(request);
-
- //3、把响应写回客户端
- PrintWriter writer=new PrintWriter(outputStream);
- writer.println(response);
- writer.flush();
-
- log=String.format("[%s:%d] req: %s; resp: %s",
- clientSocket.getInetAddress().toString(),clientSocket.getPort(),
- request,response);
- System.out.println(log);
- }
- }catch(IOException e){
- e.printStackTrace();
- }finally {
- clientSocket.close();
- }
- }
-
- private String process(String request) {
-
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- TcpThreadEchoServer server=new TcpThreadEchoServer (9090);
- server.start();
- }
- }
只有服务器这里让主线程里面循环调用accept,每次获取到一个连接,就创建一个线程,让这个线程来处理这个连接。

运行结果:

但同时使用多线程的时候,如果此时服务器频繁创建销毁线程,需要很大的成本,所以此时我们还可以用线程池来改善这个问题,就不需要频繁的创建销毁。
代码示例:
- public class TcpThreadPoolEchoServer {
- private ServerSocket listenSocket = null;
-
- public TcpThreadPoolEchoServer(int port) throws IOException {
- listenSocket = new ServerSocket(port);
- }
-
- public void start() throws IOException {
- System.out.println("服务器启动");
- ExecutorService executorService = Executors.newCachedThreadPool();
- while (true) {
- Socket clientSocket = listenSocket.accept();
- executorService.submit(new Runnable() {
- @Override
- public void run() {
- try {
- processConnection(clientSocket);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- });
- }
- }
-
- private void processConnection(Socket clientSocket) throws IOException {
- String log = String.format("[%s:%d] 客户端上线!",
- clientSocket.getInetAddress().toString(),
- clientSocket.getPort());
- System.out.println(log);
- //TCP socket也是字节流,进行数据读写时跟之前读写文件是类似的,也是使用InputStream和OutputStream
- try (InputStream inputStream = clientSocket.getInputStream();
- OutputStream outputStream = clientSocket.getOutputStream()) {
- while (true) {
- //1、读取请求并解析
- Scanner sc = new Scanner(inputStream);
- if (!sc.hasNext()) {
- log = String.format("[%s:%d] 客户端下线!",
- clientSocket.getInetAddress().toString(),
- clientSocket.getPort());
- System.out.println(log);
- break;
- }
- String request = sc.next();
-
- //2、根据请求计算响应
- String response = process(request);
-
- //3、把响应写回客户端
- PrintWriter writer = new PrintWriter(outputStream);
- writer.println(response);
- writer.flush();
-
- log = String.format("[%s:%d] req: %s; resp: %s",
- clientSocket.getInetAddress().toString(), clientSocket.getPort(),
- request, response);
- System.out.println(log);
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- clientSocket.close();
- }
- }
-
- private String process(String request) {
-
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- TcpThreadPoolEchoServer server=new TcpThreadPoolEchoServer(9090);
- server.start();
- }
- }
例子:基于TCP Socket实现一个翻译程序
- //观察之前的程序,我们可以知道,其实 echo 回显服务器和 dict 翻译服务器之间, 主要的区别就是 process 方法.
- // 其他的都差不多, 因此就可以通过继承的手段来复用代码.
- // 只需要重写 process 方法, 重新实现根据请求计算响应的逻辑即可.
- public class TcpDictServer extends TcpThreadPoolEchoServer{
- private HashMap
dict=new HashMap<>(); - public TcpDictServer(int port) throws IOException {
- super(port);
-
- dict.put("hello","你好");
- dict.put("fruit","水果");
- dict.put("animal","动物");
- }
-
- @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();
- }
- }
以上就是今天的内容,有什么问题大家都可以在评论区留言🍉🍉🍉
