精选

【技术分享】网络编程工作原理

这是 Netty 系列文章的第二篇, Netty 是一款网络编程框架,在介绍 Netty 技术之前,先来聊聊网络编程都干啥?


后端开发不用网络编程?



我们平常的开发的 Java Web 程序,都运行在 Tomcat 等 web 容器当中。所以虽然 web 应用是网络应用,但网络编程对我们来说是透明的,不被感知的。

在 servlet 中有全局域对象:request、response,用于获取 Http 请求和响应的信息;spring 中提供了 HttpServletRequest、HttpServletResponse,借助 web 容器和框架,因此我们无需直接编写网络编程和协议处理的代码,通过使用框架提供的封装好的类库,也能很方便的开发自己的业务。



网络编程怎么做?



如果没有 web 容器,要怎样实现一个简单的 http GET 请求和并返回响应呢?

talk is cheap, show your code. 上代码:

/**
* Http 服务端
*/
@Slf4j
public class SocketHttpServer {
   public static void main(String[] args) {
       new SocketHttpServer().start();
  }
   /**
    * 启动服务器
    */
   public void start() {
       int port = 8080;
       try {
           // 0.绑定端口
           ServerSocket serverSocket = new ServerSocket(port);
           log.info("服务端开始启动,端口 port: {}", port);
           Socket socket;
           // 1.循环监听 and 2.建立连接
           while (Objects.nonNull((socket = serverSocket.accept()))) {
               log.info("建立连接,客户端 IP:[{}], 端口 port:[{}]", socket.getInetAddress().getHostAddress(), socket.getPort());
               try (BufferedInputStream inputStream = new BufferedInputStream(socket.getInputStream());
                    BufferedOutputStream outputStream = new BufferedOutputStream(socket.getOutputStream())) {
                   // 3.input : 读取数据
                   byte[] buffer = new byte[1024 * 256];
                   int length = inputStream.read(buffer);
                   // 4.compute : 计算
                   // 4.1 编码
                   HttpDecoder httpDecoder = new HttpDecoder();
                   Map<String, String> request = httpDecoder.encode(buffer, length);
                   if (Objects.isNull(request)) {
                       log.info("-------------- 断开连接 --------------\n");
                       continue;
                  }
                   // 4.2 业务处理
                   log.info("url: {}", request.get("Url"));
                   log.info("method: {}", request.get("Method"));
                   log.info("data: {}", request.get("Data"));
                   String responseContent = "这是我的 http web 服务器 ^-^";
                   // 4.3 编码
                   HttpEncoder httpEncoder = new HttpEncoder();
                   byte[] response = httpEncoder.encode(responseContent);
                   // 5.output : 返回数据
                   outputStream.write(response);
                   outputStream.flush();
                   // 6.断开连接
                   log.info("-------------- 断开连接 --------------\n");
                   socket.close();
              } catch (IOException e) {
                   log.error("发生异常:", e);
              }
          }
      } catch (IOException e) {
           log.error("socket simple 服务端启动异常,{}", e.getMessage());
           e.printStackTrace();
      }
  }
}

这里使用大部分人最熟悉的 Java BIO , 这段代码能够体现网络编程服务端的过程:

  1. 创建 socket 绑定IP(服务端 IP 是 localhost)、端口;
ServerSocket serverSocket = new ServerSocket(port)
  1. 循环监听;
while (socket = serverSocket.accept()) != NULL){...}
  1. 有客户端连接上来创建连接 socket 建立连接(连接事件);
socket = serverSocket.accept()
  1. 读取数据,然后执业务处理:
byte[] buffer = new byte[1024 * 256];
int length = inputStream.read(buffer);

这里自定义了一个 http 协议解码类 HttpDecoder,从网络传输的数据是经过序列化的 “0101”这样的二进制字节流,所以要先反序列化:将字节数组转成 Java 中的对象 String,然后按 Http 协议的格式进行解析,将请求信息(请求行、method、uri、请求头、请求体数据)获取到一个 Map 中。

Map<String, String> request = httpDecoder.deEncode(buffer, length);

下面是 HTTP 协议请求数据格式:

  1. HttpDecoder 代码
@Slf4j
public class HttpDecoder {
   /**
    * 解码
    *
    * @param buffer 字节数组
    * @param length 数据长度
    * @return 转换后的 Java Map 对象
    */
   public Map<String, String> decode(byte[] buffer, int length) {
       StringBuffer httpContent = new StringBuffer();
       httpContent.append(new String(buffer, 0, length));
       log.debug("服务端接收请求:\n{}", httpContent);
       if (httpContent.length() <= 0) {
           log.info("数据不符合 http 格式,处理结束!");
           return null;
      }
     
       String[] content = httpContent.toString().split(EMPTY_LINE);
       String[] lines = content[0].split(LINE_BREAK);
       Map<String, String> request = new HashMap<>();
       // 请求行
       String header = lines[0];
       request.put("Header", header);
       String[] headerInfo = header.split(SPACE);
       request.put("Method", headerInfo[0]);
       request.put("Uri", headerInfo[1]);
       request.put("Protocol", headerInfo[2]);
       // 请求头
       for (int i = 1; i < lines.length; i++) {
           String[] line = lines[i].split(LINER_SPLIT);
           request.put(line[0], line[1]);
      }
       String url = request.get("Host") + request.get("Uri");
       request.put("Url", url);
       // 数据
       if (content.length > 1) {
           request.put("Data", content[1]);
      }
       return request;
  }
}

抽取的公共常量(协议内容分割符):

   public static final String LINE_BREAK = "\r\n";
   public static final String LINER_SPLIT = ": ";
   public static final String EMPTY_LINE = "\r\n\r\n";
   public static final String SPACE = "\\s";
  1. 然后进行业务处理

打印请求的 url, method 和 请求体数据

  1. 准备给浏览器的响应数据
HttpEncoder httpEncoder = new HttpEncoder();
byte[] response = httpEncoder.encode(responseContent);

这里自定义了一个的 http 协议编码类 HttpEecoder,encode 方法将响应给浏览器的数据 Java 字符串,按 Http 协议的格式进行组装,添加上状态行、消息包头部。

  1. http 响应格式


  1. Java 对象序列化

组装完返回给客户端的 response 对象,要通过网络传输,所以得序列化成字节流。

response.toString().getBytes(StandardCharsets.UTF_8)
  1. 协议编码类:HttpEncoder
@Slf4j
public class HttpEncoder {
   Map<String, String> response = new HashMap<>();
   public byte[] encode(String content) {
       StringBuffer response = new StringBuffer();
       // 状态行
       String statusLine = "HTTP/1.1 200 OK";
       response.append(statusLine).append(LINE_BREAK);
       // 消息包头
       response.append("Server").append(LINER_SPLIT).append("CaijuanCat").append(LINE_BREAK);
       response.append("Content-Type").append(LINER_SPLIT).append("text/plain;charset=UTF-8").append(LINE_BREAK);
       // 返回数据
       response.append(EMPTY_LINE);
       response.append(content);
       log.debug("服务端发送响应:\n{}", response);
       return response.toString().getBytes(StandardCharsets.UTF_8);
  }
}
  1. 写数据
outputStream.write(response);
outputStream.flush();
  1. 关闭连接
socket.close();

上面是用代码描述网络编程服务端的流程,如果用图示描述如下:



客户端和服务端完整流程



下图是服务端和客户端使用 socket 通信的完整过程:

客户端和服务端类似,区别在于:

  1. 服务端创建 socket 只需要指定端口,IP 就是本机,服务端 I/O 是等待连接 accept 事件;客户端 socket 要指定待连接的服务器 IP 和端口,客户端的端口是随机的,客户端 I/O 是发起连接 connect 事件;
  2. 服务端要处理若干个客户端的请求,服务端和客户端是 1:n 的关系,所在在指定端口循环监听;客户端不需要循环监听,发起请求到会话结束就关闭连接。实际场景中也是客户端有需要的时候上线,服务端 7 x 24 小时在线提供服务。



什么是 socket?



那 socket 是个啥?

某SDN: https://blog.csdn.net/github_34606293/article/details/78230456

实际上socket是对TCP/IP协议的封装,它的出现只是使得程序员更方便地使用TCP/IP协议栈而已。socket本身并不是协议,它是应用层与TCP/IP协议族通信的中间软件抽象层,是一组调用接口(TCP/IP网络的API函数)

《网络是怎样链接的》:

Socket 库是用于调用网络功能的程序组件集合。

某乎:https://www.zhihu.com/tardis/sogou/art/339230587

每一个 socket 都用一个半相关描述{协议、本地地址、本地端口}来表示;一个完整的套接字则用一 个相关描述{协议、本地地址、本地端口、远程地址、远程端口}来表示。socket 也有一个类似于打开文 件的函数调用,该函数返回一个整型的 socket 描述符,随后的连接建立、数据传输等操作都是通过 socket 来实现的;



总结



  1. 网络编程

网络编程最主要的工作就是:在发送端把信息通过规定好的协议进行组装包,在接收端按照规定好的协议把包进行解析,从而提取出对应的信息,达到通信的目的。网络编程的实质是跨主机跨网络的线程间通信

  1. socket 组件

socket是应用层与TCP/IP协议族通信的中间软件抽象层,是为了程序员开发的一组编程接口。

socket 在收发数据的操作分为若干个阶段,客户端可以大致总结为以下 4 个:

(1)创建套接字(创建套接字阶段)

(2)将管道连接到服务器端的套接字上(连接阶段)

(3)收发数据(通信阶段)

(4)断开管道并删除套接字(断开阶段)

服务器是将阶段(2)改成了等待连接,具体如下:

(1)创建套接字(创建套接字阶段)

(2-1)将套接字设置为等待连接状态(等待连接阶段)

(2-2)接受连接(接受连接阶段)

(3)收发数据(收发阶段)

(4)断开管道并删除套接字(断开阶段)

  1. 为什么要讲这个?

问题是解决方案出发的原点,技术是解决问题的工具,要明白技术为什么要那么设计和实现,就必须先明确它要解决的问题。



参考


Socket到底是什么?

HTTP协议格式详解

《网络是怎样连接的》


0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
菜卷
作者分享
因为上班拉了三个月肚子 上半年上班最大的问题,工作压力太大,导致每天频繁拉肚子,一天要跑厕所四五次。 从 2 月中旬开始,我身体开始出现告警,频繁想上厕所,每天拉肚子四五次。意识到自己身体出问题之后,先请了两天年假去了一趟海边。3.22 号,我开始看中医,第一次会诊,医生汪阿姨说:如果我的压力再大一些,我的身体可能会崩溃。 之后开始了每周去中医馆看中医,一周吃 400 多块钱 12 包中药,一直持续到 6 月 28 号最后一次。 感谢到 6 月底身体终于好转了。一共去就诊13次,看脱发一次,花了5266.95。440.08+414.34+415.44+410.44+405.58+402.16+417.57+397.87+399.64+377.45+371.20+362.86+334.72+117.60(生发町)=5266.95 3月中旬,我意识到这是比较严重的问题,问题主要在自己处理。当时和考研的小伙伴聊过,3月我们见面的时候,我发现她身体状况和我差不多,可能比我还差一点,我们在咖啡店自习,他好像出现了耳鸣,那段时间我上班也出现过。她考研初试分数出来以后,我问过她情况,大家压力都很大,互相勉励的时候,我提过:如果身体一直不好,我就准备 gap 一段时间休整。 上班和考研不一样,考研一年只能考一次,只能考一个学校,工资却是按月发的,上一个月有一个月的收入。上不下去了换个工作,不会有太大成本。 5月开始向外表达自己的身体状况和工作困境,和朋友比较简单的分享过我当时遇到的困难,我不太会和朋友分享我在工作上的困难,因为我有预感,1/3的可能我得到的回应是:你上班这么难是应该的。 也有正式的表达过,腹泻时间真的太久了,当时已经好转了,但没有完全恢复,所以期望一些外在的支持,能让问题顺利收敛,也是从那个时候开始做双周复盘。 没有和领导、同事反馈过,只在4月的时候,偷偷把企微头像换成了 p5: 也算是我无声的抱怨。 这段时间,对我很有帮助的三件事: 1.看中医,吃中药 2.和朋友住,一起生活;和朋友分享,听听朋友的建议 3.等工作情况好转,等自己适应这种工作节奏,团队运作方式和氛围 先写到这里,晚安🌙
10
年后四个月上班,开始职业倦怠了 最近要换房子,后面有更多时间独处 最近在重新整理生活,做一些调整 1.戒断社交媒体(朋友圈,视频号,B 站短视频) 2.每天打卡记录(纸质或电子) 3.没有保持适度运动 4.周末主动社交 5.复盘最近一年的生活(关注变化和进步)
3
4w/月! 外派马来西亚工作😱 昨天有一个面试,对方自称马来西亚公司: 3mixtech。做了一套90分钟的外企笔试题,之后简单面试了10分钟,其中问了DDD和TDD。当时还想外企开发理念这么先进,都实践上TDD了。半小时后告诉我过了,薪资4w/月,996,要外派马来西亚工作。让我申请旅游护照,护照下来了他们帮我订机票,让我先用旅游护照出国,说公司后续会帮我申请工作签证,我再回国取工作签证再去马来西亚办公。 给我激动的,已经开始和朋友讨论起马来西亚能取几个老婆了,但也隐隐觉得可能是诈骗。冷静之后开始了解这叫公司,发展在互联网上能查到的信息很少。在抖音有人搜过 “马来西亚it公司3mixtech” 这样的关键词,但是没有对应结果。招聘软件——猎聘上的公司地址显示西安,但是 IP 归属却是广东。一家马来西亚的公司,官网居然可以直接可以访问,而且域名对应IP归属居然是陕西西安!,种种迹象都透露着可疑的气息。 第二天我问HR能不能线下见面,她前一天还说自己在西安,结果第二天说自己在深圳,西安这边是合作公司,不是它们的员工,不方便实地考察。这时候我已经基本确定对方是骗子,然后去了招聘软件上的公司地址,不出意料没有找到她所谓的合作公司。 果然天上不会掉馅饼,遇到的馅饼十有八九都是陷阱。现在想想骗子真的太可恶了,本来行情不好找工作就不容易,骗子还出来浪费求职者的时间和精力。如果没有擦亮眼睛,真的信了骗子,可能就被骗到国外噶腰子了。 最后也借着我的经历,提醒找工作程序员们:遇到自称外企高薪招聘,要外派国外还直接给订机票的一定要警惕,小心被骗去缅北嘎腰子啊! #求职诈骗 #3mixtech
6
最近面试心态崩不住了 分享一最近找工作的情况: 1.心态崩了,看不进去东西 2.现在的面试除了最近一家难度适中,其余几家难度两次分化,简单的20分钟结束,难的一小时起步,一场面试写两道代码题 3.投不动简历了,打算再改一遍简历开始往杭州、成都投 4.笔试能力恢复到校招90%,八股文熟悉程度恢复到校招80%,各种套路问题比校招答的还溜,面试沟通比校招好不少,毕竟工作过 5.那些校招要手撕代码的,社招也要;校招不用的,社招也不用 6.目前打算:gap期控制在3个月以内,至少休息一个月,因为租房的缘故,要在9月底结束找工作 7.打算做点其他的,比如考摩托车驾照、出去玩、学新技术。
8
不用上班的第17天: 一、健康与生活 1.起的比较早,9.30之前起床 2.今天10点吃了早点 二、学习 1.看了一些 操作系统的面试题 2.看了一些 计算机网络的面试题 3.刷了3题 三、找工作 1.字节面试 2.投了一些西安比较期望的公司 3.今天找工作有点失落 四、TODO 今天很浮躁,需要做一些限制和调整 #每日打卡#
1
下载 APP