首页 文章 精选 留言 我的

精选列表

搜索[原理],共10002篇文章
优秀的个人博客,低调大师

网络编程与通信原理

总感觉这个概念,和研发有点脱节; 一、基础概念 不同设备之间通过网络进行数据传输,并且基于通用的网络协议作为多种设备的兼容标准,称为网络通信; 以C/S架构来看,在一次请求当中,客户端和服务端进行数据传输的交互时,在不同阶段和层次中需要遵守的网络通信协议也不一样; 应用层:HTTP超文本传输协议,基于TCP/IP通信协议来传递数据; 传输层:TCP传输控制协议,采用三次握手的方式建立连接,形成数据传输通道; 网络层:IP协议,作用是把各种传输的数据包发送给请求的接收方; 通信双方进行交互时,发送方数据在各层传输时,每通过一层就会添加该层的首部信息;接收方与之相反,每通过一次就会删除该层的首部信息; 二、JDK源码 在java.net源码包中,提供了与网络编程相关的基础API; 1、InetAddress 封装了对IP地址的相关操作,在使用该API之前可以先查看本机的hosts的映射,Linux系统中在/etc/hosts路径下; import java.net.InetAddress; public class TestInet { public static void main(String[] args) throws Exception { // 获取本机 InetAddress 对象 InetAddress localHost = InetAddress.getLocalHost(); printInetAddress(localHost); // 获取指定域名 InetAddress 对象 InetAddress inetAddress = InetAddress.getByName("www.baidu.com"); printInetAddress(inetAddress); // 获取本机配置 InetAddress 对象 InetAddress confAddress = InetAddress.getByName("nacos-service"); printInetAddress(confAddress); } public static void printInetAddress (InetAddress inetAddress){ System.out.println("InetAddress:"+inetAddress); System.out.println("主机名:"+inetAddress.getHostName()); System.out.println("IP地址:"+inetAddress.getHostAddress()); } } 2、URL 统一资源定位符,URL一般包括:协议、主机名、端口、路径、查询参数、锚点等,路径+查询参数,也被称为文件; import java.net.URL; public class TestURL { public static void main(String[] args) throws Exception { URL url = new URL("https://www.baidu.com:80/s?wd=Java#bd") ; printURL(url); } private static void printURL (URL url){ System.out.println("协议:" + url.getProtocol()); System.out.println("域名:" + url.getHost()); System.out.println("端口:" + url.getPort()); System.out.println("路径:" + url.getPath()); System.out.println("参数:" + url.getQuery()); System.out.println("文件:" + url.getFile()); System.out.println("锚点:" + url.getRef()); } } 3、HttpURLConnection 作为URLConnection的抽象子类,用来处理针对Http协议的请求,可以设置连接超时、读取超时、以及请求的其他属性,是服务间通信的常用方式; public class TestHttp { public static void main(String[] args) throws Exception { // 访问 网址 内容 URL url = new URL("https://www.jd.com"); HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection(); printHttp(httpUrlConnection); // 请求 服务 接口 URL api = new URL("http://localhost:8082/info/99"); HttpURLConnection apiConnection = (HttpURLConnection) api.openConnection(); apiConnection.setRequestMethod("GET"); apiConnection.setConnectTimeout(3000); printHttp(apiConnection); } private static void printHttp (HttpURLConnection httpUrlConnection) throws Exception{ try (InputStream inputStream = httpUrlConnection.getInputStream()) { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); String line ; while ((line = bufferedReader.readLine()) != null) { System.out.println(line); } } } } 三、通信编程 1、Socket Socket也被称为套接字,是两台设备之间通信的端点,会把网络连接当成流处理,则数据以IO形式传输,这种方式在当前被普遍采用; 从网络编程直接跳到Socket套接字,概念上确实有较大跨度,概念过度抽象时,可以看看源码的核心结构,在理解时会轻松很多,在JDK中重点看SocketImpl抽象类; public abstract class SocketImpl implements SocketOptions { // Socket对象,客户端和服务端 Socket socket = null; ServerSocket serverSocket = null; // 套接字的文件描述对象 protected FileDescriptor fd; // 套接字的路由IP地址 protected InetAddress address; // 套接字连接到的远程主机上的端口号 protected int port; // 套接字连接到的本地端口号 protected int localport; } 套接字的抽象实现类,是实现套接字的所有类的公共超类,可以用于创建客户端和服务器套接字; 所以到底如何理解Socket概念?从抽象类中来看,套接字就是指代网络通讯中系统资源的核心标识,比如通讯方IP地址、端口、状态等; 2、SocketServer 创建Socket服务端,并且在8989端口监听,接收客户端的连接请求和相关信息,并且响应客户端,发送指定的数据; public class SocketServer { public static void main(String[] args) throws Exception { // 1、创建Socket服务端 ServerSocket serverSocket = new ServerSocket(8989); System.out.println("socket-server:8989,waiting connect..."); // 2、方法阻塞等待,直到有客户端连接 Socket socket = serverSocket.accept(); System.out.println("socket-server:8989,get connect:"+socket.getPort()); // 3、输入流,输出流 InputStream inStream = socket.getInputStream(); OutputStream outStream = socket.getOutputStream(); // 4、数据接收和响应 byte[] buf = new byte[1024]; int readLen = 0; while ((readLen=inStream.read(buf)) != -1){ // 接收数据 String readVar = new String(buf, 0, readLen) ; if ("exit".equals(readVar)){ break ; } System.out.println("recv:"+readVar+";time:"+DateTime.now().toString(DatePattern.NORM_DATETIME_PATTERN)); // 响应数据 outStream.write(("resp-time:"+DateTime.now().toString(DatePattern.NORM_DATETIME_PATTERN)).getBytes()); } // 5、资源关闭 outStream.close(); inStream.close(); socket.close(); serverSocket.close(); System.out.println("socket-server:8989,exit..."); } } 需要注意的是步骤2输出的端口号是随机不确定的,结合jps和lsof -i tcp:port命令查看进程和端口号的占用情况; 3、SocketClient 创建Socket客户端,并且连接到服务端,读取命令行输入的内容并发送到服务端,并且输出服务端的响应数据; public class SocketClient { public static void main(String[] args) throws Exception { // 1、创建Socket客户端 Socket socket = new Socket(InetAddress.getLocalHost(), 8989); System.out.println("server-client,connect to:8989"); // 2、输入流,输出流 OutputStream outStream = socket.getOutputStream(); InputStream inStream = socket.getInputStream(); // 3、数据发送和响应接收 int readLen = 0; byte[] buf = new byte[1024]; while (true){ // 读取命令行输入 BufferedReader bufReader = new BufferedReader(new InputStreamReader(System.in)); String iptLine = bufReader.readLine(); if ("exit".equals(iptLine)){ break; } // 发送数据 outStream.write(iptLine.getBytes()); // 接收数据 if ((readLen = inStream.read(buf)) != -1) { System.out.println(new String(buf, 0, readLen)); } } // 4、资源关闭 inStream.close(); outStream.close(); socket.close(); System.out.println("socket-client,get exit command"); } } 测试结果:整个流程在没有收到客户端的exit退出指令前,会保持连接的状态,并且可以基于字节流模式,进行持续的数据传输; 4、字符流使用 基于上述的基础案例,采用字符流的方式进行数据传输,客户端和服务端只进行一次简单的交互; -- 1、客户端 BufferedReader bufReader = new BufferedReader(new InputStreamReader(inStream)); BufferedWriter bufWriter = new BufferedWriter(new OutputStreamWriter(outStream)); // 客户端发送数据 bufWriter.write("hello,server"); bufWriter.newLine(); bufWriter.flush(); // 客户端接收数据 System.out.println("client-read:"+bufReader.readLine()); -- 2、服务端 BufferedReader bufReader = new BufferedReader(new InputStreamReader(inStream)); BufferedWriter bufWriter = new BufferedWriter(new OutputStreamWriter(outStream)); // 服务端接收数据 System.out.println("server-read:"+bufReader.readLine()); // 服务端响应数据 bufWriter.write("hello,client"); bufWriter.newLine(); bufWriter.flush(); 5、文件传输 基于上述的基础案例,客户端向服务端发送图片文件,服务端完成文件的读取和保存,在处理完成后给客户端发送结果描述; -- 1、客户端 // 客户端发送图片 FileInputStream fileStream = new FileInputStream("Local_File_Path/jvm.png"); byte[] bytes = new byte[1024]; int i = 0; while ((i = fileStream.read(bytes)) != -1) { outStream.write(bytes); } // 写入结束标记,禁用此套接字的输出流,之后再使用输出流会抛异常 socket.shutdownOutput(); // 接收服务端响应结果 System.out.println("server-resp:"+new String(bytes,0,readLen)); -- 2、服务端 // 接收客户端图片 FileOutputStream fileOutputStream = new FileOutputStream("Local_File_Path/new_jvm.png"); byte[] bytes = new byte[1024]; int i = 0; while ((i = inStream.read(bytes)) != -1) { fileOutputStream.write(bytes, 0, i); } // 响应客户端文件处理结果 outStream.write("file-save-success".getBytes()); 6、TCP协议 Socket网络编程是基于TCP协议的,TCP传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,在上述案例中侧重基于流的数据传输,其中关于连接还涉及两个核心概念: 三次握手:建立连接的过程,在这个过程中进行了三次网络通信,当连接处于建立的状态,就可以进行正常的通信,即数据传输;四次挥手:关闭连接的过程,调用close方法,即连接使用结束,在这个过程中进行了四次网络通信; 四、Http组件 在服务通信时依赖网络,而对于编程来说,更常见的是的Http的组件,在微服务架构中,涉及到Http组件工具有很多,例如Spring框架中的RestTemplate,Feign框架支持ApacheHttp和OkHttp;下面围绕几个常用的组件编写测试案例; 1、基础接口 @RestController public class BizWeb { @GetMapping("/getApi/{id}") public Rep<Integer> getApi(@PathVariable Integer id){ log.info("id={}",id); return Rep.ok(id) ; } @GetMapping("/getApi_v2/{id}") public Rep<Integer> getApiV2(HttpServletRequest request, @PathVariable Integer id, @RequestParam("name") String name){ String token = request.getHeader("Token"); log.info("token={},id={},name={}",token,id,name); return Rep.ok(id) ; } @PostMapping("/postApi") public Rep<IdKey> postApi(HttpServletRequest request,@RequestBody IdKey idKey){ String token = request.getHeader("Token"); log.info("token={},idKey={}", token,JSONUtil.toJsonStr(idKey)); return Rep.ok(idKey) ; } @PutMapping("/putApi") public Rep<IdKey> putApi(@RequestBody IdKey idKey){ log.info("idKey={}", JSONUtil.toJsonStr(idKey)); return Rep.ok(idKey) ; } @DeleteMapping("/delApi/{id}") public Rep<Integer> delApi(@PathVariable Integer id){ log.info("id={}",id); return Rep.ok(id) ; } } 2、ApacheHttp public class TestApacheHttp { private static final String BASE_URL = "http://localhost:8083" ; public static void main(String[] args) { BasicHeader header = new BasicHeader("Token","ApacheSup") ; // 1、发送Get请求 Map<String,String> param = new HashMap<>() ; param.put("name","cicada") ; Rep getRep = doGet(BASE_URL+"/getApi_v2/3",header,param, Rep.class); System.out.println("get:"+getRep); // 2、发送Post请求 IdKey postBody = new IdKey(1,"id-key-我") ; Rep postRep = doPost (BASE_URL+"/postApi", header, postBody, Rep.class); System.out.println("post:"+postRep); } /** * 构建HttpClient对象 */ private static CloseableHttpClient buildHttpClient (){ // 请求配置 RequestConfig reqConfig = RequestConfig.custom().setConnectTimeout(6000).build(); return HttpClients.custom() .setDefaultRequestConfig(reqConfig).build(); } /** * 执行Get请求 */ public static <T> T doGet (String url, Header header, Map<String,String> param, Class<T> repClass) { // 创建Get请求 CloseableHttpClient httpClient = buildHttpClient(); HttpGet httpGet = new HttpGet(); httpGet.addHeader(header); try { URIBuilder builder = new URIBuilder(url); if (param != null) { for (String key : param.keySet()) { builder.addParameter(key, param.get(key)); } } httpGet.setURI(builder.build()); // 请求执行 HttpResponse httpResponse = httpClient.execute(httpGet); if (httpResponse.getStatusLine().getStatusCode() == 200) { // 结果转换 String resp = EntityUtils.toString(httpResponse.getEntity()); return JSONUtil.toBean(resp, repClass); } } catch (Exception e) { e.printStackTrace(); } finally { IoUtil.close(httpClient); } return null; } /** * 执行Post请求 */ public static <T> T doPost (String url, Header header, Object body,Class<T> repClass) { // 创建Post请求 CloseableHttpClient httpClient = buildHttpClient(); HttpPost httpPost = new HttpPost(url); httpPost.addHeader(header); StringEntity conBody = new StringEntity(JSONUtil.toJsonStr(body),ContentType.APPLICATION_JSON); httpPost.setEntity(conBody); try { // 请求执行 HttpResponse httpResponse = httpClient.execute(httpPost); if (httpResponse.getStatusLine().getStatusCode() == 200) { // 结果转换 String resp = EntityUtils.toString(httpResponse.getEntity()); return JSONUtil.toBean(resp, repClass); } } catch (Exception e) { e.printStackTrace(); }finally { IoUtil.close(httpClient); } return null; } } 3、OkHttp public class TestOkHttp { private static final String BASE_URL = "http://localhost:8083" ; public static void main(String[] args) { Headers headers = new Headers.Builder().add("Token","OkHttpSup").build() ; // 1、发送Get请求 Rep getRep = execute(BASE_URL+"/getApi/1", Method.GET.name(), headers, null, Rep.class); System.out.println("get:"+getRep); // 2、发送Post请求 IdKey postBody = new IdKey(1,"id-key") ; Rep postRep = execute(BASE_URL+"/postApi", Method.POST.name(), headers, buildBody(postBody), Rep.class); System.out.println("post:"+postRep); // 3、发送Put请求 IdKey putBody = new IdKey(2,"key-id") ; Rep putRep = execute(BASE_URL+"/putApi", Method.PUT.name(), headers, buildBody(putBody), Rep.class); System.out.println("put:"+putRep); // 4、发送Delete请求 Rep delRep = execute(BASE_URL+"/delApi/2", Method.DELETE.name(), headers, null, Rep.class); System.out.println("del:"+delRep); } /** * 构建JSON请求体 */ public static RequestBody buildBody (Object body){ MediaType mediaType = MediaType.parse("application/json; charset=utf-8"); return RequestBody.create(mediaType, JSONUtil.toJsonStr(body)) ; } /** * 构建OkHttpClient对象 */ public static OkHttpClient buildOkHttp () { return new OkHttpClient.Builder() .readTimeout(10, TimeUnit.SECONDS).connectTimeout(6, TimeUnit.SECONDS) .connectionPool(new ConnectionPool(15, 5, TimeUnit.SECONDS)) .build(); } /** * 执行请求 */ public static <T> T execute (String url, String method, Headers headers, RequestBody body, Class<T> repClass) { // 请求创建 OkHttpClient httpClient = buildOkHttp() ; Request.Builder requestBuild = new Request.Builder() .url(url).method(method, body); if (headers != null) { requestBuild.headers(headers); } try { // 请求执行 Response response = httpClient.newCall(requestBuild.build()).execute(); // 结果转换 InputStream inStream = null; if (response.isSuccessful()) { ResponseBody responseBody = response.body(); if (responseBody != null) { inStream = responseBody.byteStream(); } } if (inStream != null) { try { byte[] respByte = IoUtil.readBytes(inStream); if (respByte != null) { return JSONUtil.toBean(new String(respByte, Charset.defaultCharset()), repClass); } } catch (Exception e) { e.printStackTrace(); } finally { IoUtil.close(inStream); } } } catch (Exception e) { e.printStackTrace(); } return null; } } 4、RestTemplate public class TestRestTemplate { private static final String BASE_URL = "http://localhost:8083" ; public static void main(String[] args) { RestTemplate restTemplate = buildRestTemplate() ; // 1、发送Get请求 Map<String,String> paramMap = new HashMap<>() ; Rep getRep = restTemplate.getForObject(BASE_URL+"/getApi/1",Rep.class,paramMap); System.out.println("get:"+getRep); // 2、发送Post请求 IdKey idKey = new IdKey(1,"id-key") ; Rep postRep = restTemplate.postForObject(BASE_URL+"/postApi",idKey,Rep.class); System.out.println("post:"+postRep); // 3、发送Put请求 IdKey idKey2 = new IdKey(2,"key-id") ; restTemplate.put(BASE_URL+"/putApi",idKey2,paramMap); // 4、发送Delete请求 restTemplate.delete(BASE_URL+"/delApi/2",paramMap); // 5、自定义Header请求 HttpHeaders headers = new HttpHeaders(); headers.add("Token","AdminSup"); HttpEntity<IdKey> requestEntity = new HttpEntity<>(idKey, headers); ResponseEntity<Rep> respEntity = restTemplate.exchange(BASE_URL+"/postApi", HttpMethod.POST, requestEntity, Rep.class); System.out.println("post-header:"+respEntity.getBody()); } private static RestTemplate buildRestTemplate (){ // 1、参数配置 SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setReadTimeout(3000); factory.setConnectTimeout(6000); // 2、创建对象 return new RestTemplate(factory) ; } } 五、参考源码 编程文档: https://gitee.com/cicadasmile/butte-java-note 应用仓库: https://gitee.com/cicadasmile/butte-flyer-parent

优秀的个人博客,低调大师

KVC原理与数据筛选

作者:宋宏帅 1 前言 在技术论坛中看到一则很有意思的 KVC 案例: interface Person : NSObject@property (nonatomic, copy) NSString *name;@property (nonatomic, assign) NSInteger age;@endPerson *person = [Person new];person.name = @"Tom";person.age = 10;[person setValue:@"100" forKey:@"age"];//此处赋值为字符串,类中属性为Integer 第一反应是崩溃,因为 OC 是类型敏感的。可是自己实现并打印后的结果出于意料,没有崩溃且赋值成功。所以有了深入了解 KVC 的内部实现的想法! 2 什么是 KVC key-value-coding:键值编码,一种可以通过键名间接访问和赋值对象属性的机制 KVC 是通过 NSObject、NSArray、NSDictionary 等的类别来实现的主要方法包括一下几个: - (nullable id)valueForKey:(NSString *)key;- (void)setValue:(nullable id)value forKey:(NSString *)key;- (void)setNilValueForKey:(NSString *)key;- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;- (nullable id)valueForUndefinedKey:(NSString *)key; 3 KVC 执行分析 那么上面的案例中的- (void)setValue:(nullable id)value forKey:(NSString *)key;是怎样的执行过程呢?借助反汇编工具获得 Foundation.framework 部分源码(为了解决和系统 API 冲突问题增加前缀_d,NS 替换为 DS),以此分析 KVC 执行过程。(流程中的边界判断等已经忽略,如想了解可以参考源码,本文只探究主流程。) 3.1 设置属性 3.1.1 查找访问器方法或成员变量 + (DSKeyValueSetter *)_d_createValueSetterWithContainerClassID:(id)containerClassID key:(NSString *)key { DSKeyValueSetter *setter = nil; char key_cstr_upfirst[key_cstr_len + 1]; key_cstr[key_cstr_len + 1]; ... Method method = NULL; //按顺序寻找set<Key>,_set<Key>,setIs<Key>。找到后则生成对应的seter if ((method = DSKeyValueMethodForPattern(self, "set%s:", key_cstr_upfirst)) || (method = DSKeyValueMethodForPattern(self, "_set%s:", key_cstr_upfirst)) || (method = DSKeyValueMethodForPattern(self, "setIs%s:", key_cstr_upfirst)) ) { //生成Method:包含selector,IMP。返回和参数类型字符串 setter = [[DSKeyValueMethodSetter alloc] initWithContainerClassID:containerClassID key:key method:method]; } else if ([self accessInstanceVariablesDirectly]) {//如果没有找到对应的访问器方且工厂方法accessInstanceVariablesDirectly == ture ,则按照顺序查找查找成员变量_<key>,_is<Key>,<key>,is<Key>(注意key的首字母大小写,查找到则生成对应的setter) Ivar ivar = NULL; if ((ivar = DSKeyValueIvarForPattern(self, "_%s", key_cstr)) || (ivar = DSKeyValueIvarForPattern(self, "_is%s", key_cstr_upfirst)) || (ivar = DSKeyValueIvarForPattern(self, "%s", key_cstr)) || (ivar = DSKeyValueIvarForPattern(self, "is%s", key_cstr_upfirst)) ) { setter = [[DSKeyValueIvarSetter alloc] initWithContainerClassID:containerClassID key:key containerIsa:self ivar:ivar]; } } ... return setter;} 查找顺序如下: 查找访问器方法:set,_set,setIs 如果步骤 1 中没找到对应的方法且 accessInstanceVariablesDirectly == YES 则查找顺序如下:_,_is,,is 查找不到则调用 valueForUndefinedKey 并抛出异常 3.1.2 生成 setter + (DSKeyValueSetter *)_d_createOtherValueSetterWithContainerClassID:(id)containerClassID key:(NSString *)key { return [[DSKeyValueUndefinedSetter alloc] initWithContainerClassID:containerClassID key:key containerIsa:self];} //构造方法确定方法编号 d_setValue:forUndefinedKey: 和方法指针IMP _DSSetValueAndNotifyForUndefinedKey- (id)initWithContainerClassID:(id)containerClassID key:(NSString *)key containerIsa:(Class)containerIsa { ... return [super initWithContainerClassID:containerClassID key:key implementation:method_getImplementation(class_getInstanceMethod(containerIsa, @selector(d_setValue:forUndefinedKey:))) selector:@selector(d_setValue:forUndefinedKey:) extraArguments:arguments count:1];} 3.1.3 赋值 基本的访问器方法、变量的查找和异常处理已经清楚的知道了。那么上面的例子是如何出现的呢?明明传入的是字符串,最后赋值的时候转变为访问器方法所对应的类型?继续刨根问底! DSKeyValueSetter 对象已经生成,即确定了发送消息的对象 object、访问器方法名 SEL、访问器函数指针 IMP、以及使用 KVC 时传入的 Key 和 Value。下面进入方法调用阶段:_DSSetUsingKeyValueSetter(self,setter, value); IMP 指针为_DSSetIntValueForKeyWithMethod 其定义如下:之所以有文章开头提到的效果就是这里起了作用,在 IMP 调用的时候做了[value valueGetSelectorName],将对应的 NSNumber 转换为简单数据类型。这里是 intValue。 void _DSSetIntValueForKeyWithMethod(id object, SEL selector,id value, NSString *key, Method method) {// object:person selector:setAge: value:@(100) key:age method:selector + IMP + 返回类型和参数类型 即_extraArgument2,其在第一步查找到访问器方法后生成 __DSSetPrimitiveValueForKeyWithMethod(object, selector, value, key, method, int, intValue);}#define __DSSetPrimitiveValueForKeyWithMethod(object, selector, value, key, method, valueType, valueGetSelectorName) do {\ if (value) {\ void (*imp)(id,SEL,valueType) = (void (*)(id,SEL,valueType))method_getImplementation(method);\ imp(object, method_getName(method), [value valueGetSelectorName]);\调用person的setAge:方法。参数为100 }\ else {\ [object setNilValueForKey:key];\ }\}while(0)//如果第一步中没有找到访问器方法只找到了成员变量则直接执行赋值操作void _DSSetIntValueForKeyInIvar(id object, SEL selector, id value, NSString *key, Ivar ivar) { if (value) { *(int *)object_getIvarAddress(object, ivar) = [value intValue]; } else { [object setNilValueForKey:key]; }} 起始问题完美解决!执行流程如下: 3.2 取值 3.2.1 查找访问器方法或成员变量 + (DSKeyValueGetter *)_d_createValueGetterWithContainerClassID:(id)containerClassID key:(NSString *)key { DSKeyValueGetter * getter = nil; ... Method getMethod = NULL; if((getMethod = DSKeyValueMethodForPattern(self,"get%s",keyCStrUpFirst)) || (getMethod = DSKeyValueMethodForPattern(self,"%s",keyCStr)) || (getMethod = DSKeyValueMethodForPattern(self,"is%s",keyCStrUpFirst)) || (getMethod = DSKeyValueMethodForPattern(self,"_get%s",keyCStrUpFirst)) || (getMethod = DSKeyValueMethodForPattern(self,"_%s",keyCStr))) { getter = [[DSKeyValueMethodGetter alloc] initWithContainerClassID:containerClassID key:key method:getMethod]; }// 查找对应的访问器方法 ... else if([self accessInstanceVariablesDirectly]) {//查找属性 Ivar ivar = NULL; if((ivar = DSKeyValueIvarForPattern(self, "_%s", keyCStr)) || (ivar = DSKeyValueIvarForPattern(self, "_is%s", keyCStrUpFirst)) || (ivar = DSKeyValueIvarForPattern(self, "%s", keyCStr)) || (ivar = DSKeyValueIvarForPattern(self, "is%s", keyCStrUpFirst)) ) { getter = [[DSKeyValueIvarGetter alloc] initWithContainerClassID:containerClassID key:key containerIsa:self ivar:ivar]; } } } if(!getter) { getter = [self _d_createValuePrimitiveGetterWithContainerClassID:containerClassID key:key]; } return getter;} 按照 get,,is,_的顺序查找成员方法 如果 1.没有找到对应的方法且 accessInstanceVariablesDirectly==YES,则继续查找成员变量,查找顺序为_,_is,,is 如果 1,2 没有找到对应的方法和属性则调用 valueForUndefinedKey:并抛出异常 3.2.2 如上步骤没定位到访问器方法或成员变量则走下面的流程生成对应的 getter 访问器方法生成IMP- (id)initWithContainerClassID:(id)containerClassID key:(NSString *)key method:(Method)method { NSUInteger methodArgumentsCount = method_getNumberOfArguments(method); NSUInteger extraAtgumentCount = 1; if(methodArgumentsCount == 2) { char *returnType = method_copyReturnType(method); IMP imp = NULL; switch (returnType[0]) { ... case 'i': { imp = (IMP)_DSGetIntValueWithMethod; } break; ... free(returnType); if(imp) { void *arguments[3] = {0}; if(extraAtgumentCount > 0) { arguments[0] = method; } return [super initWithContainerClassID:containerClassID key:key implementation:imp selector:method_getName(method) extraArguments:arguments count:extraAtgumentCount]; }} 单步调试后可以看到具体的 IMP 类型 定义如下: NSNumber * _DSGetIntValueWithMethod(id object, SEL selctor, Method method) {// return [[[NSNumber alloc] initWithInt: ((int (*)(id,SEL))method_getImplementation(method))(object, method_getName(method))] autorelease];} 3.2.3 取值 取值调用如下: 4 简单数据类型 KVC 包装和拆装关系 NSNunber: NSValue 5 KVC 高级 修改数组中对象的属性[array valueForKeyPath:@”uppercaseString”]利用 KVC 可以批量修改属性的成员变量值 求和,平均数,最大值,最小值 NSNumbersum= [array valueForKeyPath:@”@sum.self”];NSNumberavg= [array valueForKeyPath:@”@avg.self”];NSNumbermax= [array valueForKeyPath:@”@max.self”];NSNumbermin= [array valueForKeyPath:@”@min.self”]; 6 数据筛选 经过上面的分析可以明白 KVC 的真正执行流程。下面结合日常工程中的实际应用来优雅的处理数据筛选问题。使用 KVC 处理可以减少大量 for 的使用并增加代码可读性和健壮性。如图所示: 项目中的细节如下:修改拒收数量时更新总妥投数和总拒收数、勾选明细更新总妥投数和总拒收数、全选、清空、反选。如果用通常的做法是每次操作都要循环去计算总数和记录选择状态。下面是采用 KVC 的实现过程。模型涉及: @property (nonatomic,copy)NSString* skuCode;@property (nonatomic,copy)NSString* goodsName;@property (nonatomic,assign)NSInteger totalAmount;@property (nonatomic,assign)NSInteger rejectAmount;@property (nonatomic,assign)NSInteger deliveryAmount;///单选用@property (nonatomic, assign) BOOL selected; 1)更新总数 - (void)updateDeliveryInfo { //总数 NSNumber *allDeliveryAmount = [self.orderDetailModel.deliveryGoodsDetailList valueForKeyPath:@"@sum.totalAmount"]; //妥投数 NSNumber *allRealDeliveryAmount = [self.orderDetailModel.deliveryGoodsDetailList valueForKeyPath:@"@sum.deliveryAmount"]; //拒收数 NSNumber *allRejectAmount = [self.orderDetailModel.deliveryGoodsDetailList valueForKeyPath:@"@sum.rejectAmount"];} 2)全选[self.orderDetailModel.deliveryGoodsDetailList setValue:@(YES) forKeyPath:@”selected”]; 3)清空[self.orderDetailModel.deliveryGoodsDetailList setValue:@(NO) forKeyPath:@”selected”]; 4)反选 NSPredicate *selectedPredicate = [NSPredicate predicateWithFormat:@"selected == %@",@(YES)];NSArray *selectedArray = [self.orderDetailModel.deliveryGoodsDetailList filteredArrayUsingPredicate:selectedPredicate];NSPredicate *unSelectedPredicate = [NSPredicate predicateWithFormat:@"selected == %@",@(NO)];NSArray *unSelectedArray = [self.orderDetailModel.deliveryGoodsDetailList filteredArrayUsingPredicate:unSelectedPredicate];[selectedArray setValue:@(NO) forKeyPath:@"selected"];[unSelectedArray setValue:@(YES) forKeyPath:@"selected"]; 7 总结 KVC 在处理简单数据类型时会经过数据封装和拆装并转换为对应的数据类型。通过 KVC 的特性我们可以在日常使用中更加优雅的对数据进行筛选和处理。优点如下:可阅读性更高,健壮性更好。

优秀的个人博客,低调大师

NameServer 核心原理解析

在之前的文章中,已经把 Broker、Producer 和 Conusmer 的部分源码和核心的机制介绍的差不多了,但是其实 RocketMQ 中还有一个比较关键但是我们平时很容易忽略的组件——NameServer。 在日常的使用中,我们接触的最多的还是 Producer 和 Consumer,而 NameServer 没有直接跟我们有交互。就像 Kafka 集群背后用于其集群元数据管理的 Zookeeper 集群一样,NameServer 也在背后支撑着 RocketMQ 正常工作。 你给翻译翻译,什么叫 NameServer NameServer 你可以简单的把它理解成注册中心。 Broker 启动的时候会将自己注册到 NameServer 中,注册的同时还会将 Broker 的 IP 地址、端口相关的数据,以及保存在 Broker 中的 RocketMQ 集群路由的数据一并跟随心跳发送到 NameServer。这里的路由信息是指 Topic 下的 MessageQueue 分别都在哪台 Broker 上。 而 Producer 则会从 NameServer 中获取元数据,从而将 Message 发到对应的 Broker 中去。 相应的,Consumer 也需要从 NameServer 中获取数据。平常我们配置消费者,里面重要的信息主要就两个,分别是你要消费的 Topic 和当前的 Consumer Group。根据配置,Consumer 会去 NameServer 获取对应的 Topic 都有哪些 Broker,其真实的 IP 地址和端口是多少,拿到了这个之后就可以开始进行消息消费了。 注册 Broker 都做了什么 这里我们先通过注册 Broker 的源码来预热一下,为后面阅读整个部分的源码做准备,直接上代码。 首先这里做了一个对 Broker 版本的区分,不同的版本采用不同的处理方式,鉴于官网现在最新的版本都已经到了 4.9.0 了,就暂时先不考虑低版本的情况了,后面有时间再讨论。 只有向上面那种几行的代码会给大家贴出来,其余的代码我会尽量用流程图代替 校验 Body 的完整性 首先是校验 Broker 传过来的数据的完整性。很简单的一个判断,将 Broker 传过来的 Body 用 CRC32算法 加密之后,和请求中 Header 中所带的由 Broker 加密的值进行对比,不同的话就说明数据的完整性出了问题,接下来需要中断注册流程。 解析Body 这里分成两种情况: Body为空 Body不为空 如果 Body 为空,则会将当前要注册的 Broker 的 DataVersion 给重置; 而 **Body 不为空 **则会进行对 Body 进行解析,主要是从中解析出 DataVersion ,代表 Broker 中的数据版本。其次解析出这个 Broker 中存储的所有 Topic 及其相关的配置。 执行注册逻辑 这里就是注册的核心逻辑了,这里为了更加容易理解,我们来分情况讨论,就不把两种情况揉在一起了。 首次注册 非首次注册 维护集群中 Broker 的 Name 在整个操作开始之前,会先给 RouteInfoManager 加一把锁,这个 RouteInfoManager 里面就是 NameServer 存储数据的地方。这个锁是个读写锁,使用的是 Java 中的 ReentrantReadWriteLock。 这里的 BrokerName 是在 RocketMQ 配置文件中配置的变量。就是用于标识一个 Broker 的名字,但我们知道 Broker 是有主从架构的,并且 RocketMQ 4.5 之后推出的 Dleger 可以实现一主多从,换句话说,一个 Broker Name 可能会对应多个 Broker 实例。 在 MQ 看来,Broker 是多实例部署的;而在 Producer 或者 Consumer 来看,Broker就只有一个。所以,这个步骤内所维护的就是在当前集群中,有多少个这样的 Broker Name。 维护 Broker 的数据 然后,RocketMQ 会在 brokerAddrTable 中维护每个 Broker 的核心数据,包含: Broker 所处的集群 Broker 的名字(上面刚刚讨论过) 所有 Broker 的 BrokerID 和 Address 的对应关系,是个 Map,Address 为 IP+端口 同一个 Broker Name 下,为什么会有多个地址信息已经在上个步骤解答过,不在此赘述。 Broker 的数据维护主要有两个方面: 该 Broker 数据在 brokerAddrTable 中是否存在 brokerAddrTable 中维护的数据不能有重复的地址信息 第一个过于基础简单,就不再赘述。我们重点看第二个点,我们知道会有多个 Broker 地址,存在一个 Map 中,因为 Broker 是基于主从架构。那不知道你有没有想过,NameServer 如何区分 主 和 从 的呢? 答案是通过 Map 的 Key,如果是 0 则代表是 Master 节点,1 则代表 Slave 节点,因为 RocketMQ 自己实现的 Broker 主从架构是一主一从,而一主多从则是由 RocketMQ 4.5 之后加入的 Dleger 实现的,暂时先不讨论。区分的逻辑如下图: 那什么时候会出现重复呢? 答案是主从切换 举个例子,假设某个 Slave Broker 的 Address 为 192.168.1.101:8081 ,且已经注册。此时brokerAddrs 中已经有一个key: 1 value: 192.168.1.101:8081 记录了。 当集群中的 Master 宕机之后,会进行故障恢复,假设选中了上面这个 Broker 为新的 Master,在进行注册的时候会发现,brokerAddrs 中已经有一个同样的 Address 了,只是 Key 不同。但是由于它们从本质上来说就是同一台机器,如果不将 key 为1,也就是角色为 Slave 的记录去掉,就会造成数据一致性的问题。 简单总结一下来说,同一个 Adreess,在 brokerAddrs 中只能存在一个。感兴趣的可以看一下源码,其实跟上面文字描述的逻辑是一样的。 去除了重复的 Address 数据之后,就会将本次注册的 Broker 的数据注册进 brokerAddrs 中。 维护 MessageQueue 的数据 这里主要是根据 Broker 的数据更新其 MessageQueue 相关的数据。接下来,我们详细解析一下 Message Queue 的维护流程,同样会给出源码和流程图,两部分等价,可选择性观看。 当 Master 节点来注册时,如果是首次注册或者数据有更新,便会调用一个方法createAndUpdateQueueData去维护 MessageQueue 相关的数据。这里对数据是否更新的判断,是基于 DataVersion 的,代表 Broker 数据的版本。 此后通过 Topic 的 Name 拿到对应的 MessageQueue 的列表,这里可能会有点疑问,一个 Topic 难道不应该只有一个对 MessageQueue 相关的配置吗,为什么这里拿到的是个列表? 小了,格局小了 Topic 是个逻辑上的概念,一个 Topic 的 MessageQueue 会分布在不同的 Broker 上,所有这里是个列表。 更新的流程如上图,拿到了 MessageQueue 的列表之后,会和本次注册的 Broker 中的 MessageQueue 数据做一个对比,如果发现不同就进行全量的替换,没什么其他的复杂对比逻辑。源码等同上图,感兴趣的可以自行查看。 维护 Broker 的存活信息 到这里,MessageQueue 相关的逻辑就处理完了,接下来 NameServer 会再去更新 brokerLiveTable 中的数据,这里存放了当前正在活跃的所有 Broker。这块的作用后续会讲。 NameServer 启动流程 上面通过了解注册 Broker的整个流程,对整个 NameServer 的架构有了个大概的了解,接下来再从整体视角来看一下 NameServer。 整体的流程上面这张图已经给出来了,就不放源码了,意义不大。 这里说一下扫描不再活跃的Broker,这个后台线程会每 10秒 钟执行一次,这里会对上文提到的 brokerLiveTable 进行遍历处理,因为这里面维护了所有的正在活跃的 Broker。 如果某个 Broker 超过了 120秒 没有发送心跳给 NameServer,就会将其从 brokerLiveTable 中移除。 NameServer 可处理的操作 上面简单了解了 注册 Broker 的流程,实际上 NameServer 还支持很多其他的操作,这里就不再这里列出来了,看了没有意义,感兴趣的可以自己去网上找,一大堆的资料。而且 Register Broker 这个操作中所涉及到源码中的数据结构,其他的操作都会用到,所以了解了 Register Broker 之后,再去阅读其他操作的源码会非常的顺。 好了以上就是本篇博客的全部内容了,欢迎微信搜索关注【SH的全栈笔记】,回复【队列】获取MQ学习资料,包含基础概念解析和RocketMQ详细的源码解析,持续更新中。 如果你觉得这篇文章对你有帮助,还麻烦点个赞,关个注,分个享,留个言。

优秀的个人博客,低调大师

OSPF基本原理

​​ OSPF协议是常见、最重要的协议之一,必须充分的理解掌握,本文是对前文的总结和补充,后文也会继续描述OSPF协议。本次没有配置案例,下期补充。 一、OSPF的三张表 1. 邻居表:记录邻居状态和关系; 2. 拓扑表:链路状态数据库; 3. 路由表:记录由SPF算法计算的路由。 二、OSPF路由器类型 1.区域内路由器(IR):所有接口都在同一区域; 2.骨干路由器(BR):有接口在骨干区域; 3.区域边界路由器(ABR):连接骨干区域和非骨干区域; 4.自治系统边界路由器(ASBR):连接外部自治系统,并引入外部路由 路由器类型示图: ​ 三、OSPF网络类型 1. 定义 对于不同的二层链路类型的网段,OSPF会生成不同的网络类型;不同的网络类型,DR/BDR选举,LSA细节,协议报文发送形式等会有所不同。 2. 类型 2.1 Broadcast 广播网络,以太网默认的网络类型; 组播发送协议报文:224.0.0.5 所有运行OSPF的接口会监听,224.0.0.6 所有DR/BDR的接口会监听; 需要选举DR/BDR; hello-time 10秒,dead-time 40秒 2.2 NBMA 非广播多点可达,帧中继默认的网络类型; 单播发送协议报文,需要手动指定邻居; 需要选举DR/BDR; hello-time30秒,dead-time120秒 2.3 P2MP 点到多点网络,由其他网络类型手动更改; 模拟组播发送协议报文,可以自动发现邻居; 不选举DR/BDR; hello-time30秒,dead-time120秒 2.4P2P 点到点网络,PPP默认的网络类型; 组播发送协议报文; 不选举DR/BDR; hello-time 10秒,dead-time 40秒 说明:可以通过将默认网络类型改成P2P,加快ospf收敛速度 查看ospf网络类型命令:dis ospf interface g 0/1 ​ 修改ospf网络类型命令:ospf network-type ? ​ 四、OSPF邻接建立过程 1. Down:关闭状态(稳定状态) 2.Init:单方发现状态,收到了对方的hello报文,但没有收到对方的hello确认报文 3. Attempt:只会出现NBMA网络中(已淘汰,不需要考虑),发出了Hellow包,但未收到对方的Hellow包 4. 2-way:邻居状态(稳定状态),邻居双方互相发现,并确认了DR/BDR角色,当选举完毕,就算出现一台更高优先级的路由器,也不会替换成为新的DR/BDR,需要原DR/BDR失效,或者重置OSPF进程才会成为新的DR/BDR 2-way的前提: 说明:非常重要 (1)Router-id无冲突:修改Router-id需要重置OSPF进程使生效 (2)掩码长度一致; (3)区域id一致; (4)验证密码一致; (5)hello-time一致; (6)dead-time一致; 5. Exstart:交换开始状态,发送第一个DD报文,不传送LSDB摘要,仅用于确定LSA传递的主从;DD报文置位符:I Init位,置位则表示此报文是第一个DD报文 M more位,置位则表示还有后续DD报文 MS master位,置位则表示本端为主 6. Exchange:交换状态,发送后续DD报文,用于通告LSDB摘要 7. Loading:读取状态,进行LSA的请求、加入和确认 8. Full:邻接状态(稳定状态),两端LSDB同步 Full的前提 说明:非常重要 (1)两端网络类型一致,否则邻居状态FULL,但无法学习路由 (2)两端MTU一致,否则邻居状态卡在Exstart、Exchange ​ 五、LSDB更新 更新流程 1. 收到LSA更新,在本地LSDB中查询此LSA,未查到则加入LSDB 2. 查到此LSA,则把收到的LSA和本地LSA进行序列号对比 3. 如本地LSA序列号大,则丢弃更新,否则加入LSDB 说明:LSA越大,说明越新 广播网络中的更新 只由DR发起更新 六、OSPF开销计算 参考带宽 计算开销的基准带宽值,默认参考带宽是100M 建议把网络中最高的链路带宽设置为参考带宽 计算方法 链路带宽大于等于参考带宽cost=1 链路带宽小于参考带宽cost=参考带宽/链路带宽(Mb) 说明:余数取整加1(cost值都是整数) 七、常用命令 (1)[h3c]router id 'router-id' 配置全局Router-id,对所有协议生效,协议内指定Router-id的优先级更高 (2)[h3c]display ospf interface 查看OSPF接口信息,网络类型通过此命令查看 (3)[h3c]display ospf routing 查看OSPF路由信息,可以查看路由所属区域和发布者 (4)[h3c]display ospf statistics error 查看OSPF报错信息 说明:ospf排除神器 (5)[h3c]display ospf lsdb 查看本地LSDB汇总信息 (6)[h3c-GigabitEthernet0/0]ospf network-type 'broadcast/nmba/p2mp/p2p' 配置接口网络类型 (7)[h3c-GigabitEthernet0/0]ospf timer hello 'seconds' 配置接口HELLO时间 (8)[h3c-GigabitEthernet0/0]ospf timer dead 'seconds' 配置接口DEAD时间 (9)[h3c-ospf-1]Bandwidth-reference 'value' 配置参考带宽 (10)[h3c-ospf-1]default-route-advertise 将已经存在的默认路由引入到OSPF (11)[h3c-ospf-1]default-route-advertise always 自动产生一条默认路由LSA下发到OSPF ​ 附: 关注我,加微信,获取此次配置工程、更多配置案例,也可以承接远程调试各厂商交换机、路由器、防火墙等私活,欢迎各位老板加微信私聊。

优秀的个人博客,低调大师

MySQL binlog原理及应用

01概述 Binlog它记录了所有的DDL和DML(除了数据查询语句)语句,以事件(EVENT)形式记录,还包含语句所执行的消耗的时间,MySQL的二进制日志是事务安全型的。 一般来说开启二进制日志大概会有1%的性能损耗(MySQL官方测试数据),虽然会降低性能,但是binlog可以用于主从复制(replication)和point-in-time的恢复,所以还是很有必要开启的。 查看: SHOWBINLOGEVENTIN'mysqld.00001'\G; 配置: 通过参数log-bin[=name]可以开启二进制日志,如果不指定name,默认二进制日志文件名为主机名,后缀名为二进制日志的序列号,所在路径为数据库所在目录(datadir)。 02分类 二进制日志包括两类文件: 1、二进制日志索引文件(文件名后缀为.index)用于记录所有的二进制文件。 2、二进制日志文件(文件名后缀为.00000*)记录数据库所有的DDL和DML(除了数据查询语句)语句事件。 03分类 binlog有三种格式: 1、基于SQL语句的复制(statement-based replication, SBR) 2、基于行的复制(row-based replication, RBR) 3、混合模式复制(mixed-based replication, MBR) 3.1 Statement 记录的是逻辑SQL,每一条修改操作的sql都会记录在binlog中。 优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO, 提高了性能。 缺点:无法完全保证slave节点与master节点数据完全一致。像一些特定函数的功能,slave可与master上要保持一致会有很多相关问题。 3.2 Row 5.1.5版本的MySQL开始支持row level的复制,它不记录sql语句上下文相关信息,仅保存哪条记录被修改。 优点:binlog中可以不记录执行的sql语句的上下文相关的信息,仅需要记录那一条记录被修改成什么了。row格式的日志内容会非常清楚的记录下每一行数据修改的细节,不会出现某些特定情况下的存储过程。 缺点:所有的执行的语句当记录到日志中的时候,都将以每行记录的修改来记录,这样可能会产生大量的日志内容。 3.3 Mixed 从5.1.8版本开始,MySQL提供了Mixed格式,实际上就是Statement与Row的结合。 在Mixed模式下,(默认情况下)一般的语句修改使用statment格式保存binlog,下面的情况会采用row格式的情况包括: 1、表的存储引擎为NDB; 2、使用了UUID()、USER()、CURRENT_USER()、FOUND_ROWS()、ROW_COUNT()等不确定函数; 3、使用了INSERT DELAY语句; 4、使用了用户定义函数(UDF); 5、使用了临时表(temporary table)。 注:不需要死记硬背这些情况会使用row,之所以不使用statement模式,就是因为简单的逻辑SQL无法实现数据的回放,比如使用UUID(),这个是随机的,无法保证每次结果都一样,为了保证主从复制数据一致性,则必须是记录数据的变化信息。 04参数 4.1 max_binlog_size 指定了单个二进制日志文件最大值,如果超过该值,则产生新的二进制日志文件后缀名+1,并记录到.index文件。 4.2 binlog_cache_size 控制缓冲大小,默认大小32K,基于会话的,因此每开启一个事务就分配一个binlog_cache_size大小的缓存,所以不能设置过大。 当一个事务的记录大于binlog_cache_size时,MySQL会把缓冲中的日志写入一个临时文件中,因此该值又不能设置太小(否则会频繁刷盘)。 4.3 sync_binlog 表示每写缓冲多少次就要同步到磁盘。如果设置为1,表示采用同步写磁盘的方式来写二进制日志,这时候写操作不使用操作系统的缓冲来写二进制日志。sync_binlog的默认值为0,如果使用InnoDB存储引擎进行复制,并且想得到最大的可用性,建议将该值设置为ON(对数据库IO系统带来一定影响)。 4.4 binlog-do-db 表示需要写入哪些库的日志,默认为空,表示需要同步所有库的日志到二进制日志。 4.5 binlog-ignore-db 表示需要忽略写入哪些库的日志,默认为空,表示需要同步所有库的日志到二进制日志。 4.6 log-slave-update 若当前数据库是主从复制架构中的slave节点,则它不会将从master取得并执行的binlog写入自己的二进制文件中。但是,如果需要搭建master->slave->slave这种架构的复制,则必须设置该参数。 4.7 binlog_format 记录二进制日志的格式。 05操作 5.1 开启 1、vi编辑打开mysql配置文件 # vi /usr/local/mysql/etc/my.cnf在[mysqld] 区块设置/添加 log-bin=mysql-bin 重启mysqld服务使配置生效 # /usr/local/mysql/bin/mysqld_safe --user=mysql & 2、也可登录mysql服务器,通过mysql的变量配置表,查看二进制日志是否已开启 5.2 查看日志列表 mysql> show master logs; 5.3 查看节点状态 查看主节点状态信息: mysql> show master status; 查看从节点状态信息: mysql> show slave status; 5.4 刷新log日志 mysql> flush logs; 注:每当mysqld服务重启时,会自动执行此命令,刷新binlog日志;在mysqldump备份数据时加 -F 选项也会刷新binlog日志; 5.5 清空日志 mysql> reset master; 5.6 启动复制 mysql> start slave 可以指定线程类型:IO_THREAD,SQL_THREAD,如果不指定,两个都启动。 06应用场景 binlog主要使用场景: 1、主从复制:在master节点开启binlog,并把它的二进制日志传递给slaves,slave节点通过复制和执行二进制日志进行实时同步。 注:具体工作流程参考《MySQL主从复制》。 2、数据恢复:某些数据的恢复需要二进制日志,例如,如果需要恢复数据库全量备份的文件,可以通过二进制日志进行point-in-time恢复。 3、审计(audit):用户通过二进制日志中的信息来进行审计,判断是否有对数据库进行注入的攻击。

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册