百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

Istio 中实现客户端源 IP 的保持

liebian365 2025-02-09 13:09 18 浏览 0 评论

导语

对于很多后端服务业务,我们都希望得到客户端资源 IP。云上的负载均衡器,比如,腾讯云 CLB 支持将客户端源IP传递到后端服务。但在使用 istio 的时候,由于 istio ingressgateway 以及 sidecar 的存在,后端服务如果需要获取客户端源 IP,特别是四层协议,情况会变得比较复杂。

正文

很多业务场景,我们都希望得到客户端资源 IP。云上负载均衡器,比如,腾讯云 CLB支持将客户端 IP 传递到后端服务。TKE/TCM 也对该能力做了很好的集成。

但在使用 istio 的时候,由于中间链路上,istio ingressgateway 以及 sidecar 的存在,后端服务如果需要获取客户端 IP,特别是四层协议,情况会变得比较复杂。

对于应用服务商来说,它只能看到 Envoy 过来的连接。

一些常见的源 IP 保持方法

先看看一些常见 Loadbalancer/Proxy 的源 IP 保持方法。我们的应用协议一般都是四层、或者七层协议。

七层协议的源 IP 保持

七层的客户端源 IP 保持方式比较简单,最具代表性的是 HTTP 头XFF(X-Forwarded-For),XFF 保存原始客户端的源 IP,并透传到后端,应用可以解析 XFF 头,得到客户端的资源 IP。常见的七层代理组件,比如 Nginx、Haproxy,包括 Envoy 都支持该功能。

四层协议的源 IP 保持

DNAT

IPVS/iptables都支持 DNAT,客户端通过 VIP 访问 LB,请求报文到达 LB 时,LB 根据连接调度算法选择一个后端 Server,将报文的目标地址 VIP 改写成选定 Server 的地址,报文的目标端口改写成选定 Server 的相应端口,最后将修改后的报文发送给选出的 Server。由于 LB 在转发报文时,没有修改报文的来源 IP,所以,后端 Server 可以看到客户端的资源 IP。

Transparent Proxy

Nginx/Haproxy 支持透明代理(Transparent Proxy)。当开启该配置时,LB 与后端服务建立连接时,会将 socket 的源 IP 绑定为客户端的 IP 地址,这里依赖内核TPROXY以及 socket 的 IP_TRANSPARENT 选项。

此外,上面两种方式,后端服务的响应必须经过 LB,再回到 Client,一般还需要策略路由的配合。

TOA

TOA(TCP Option Address)是基于四层协议(TCP)获取真实源 IP 的方法,本质是将源 IP 地址插入 TCP 协议的 Options 字段。这需要内核安装对应的TOA内核模块。

Proxy Protocol

Proxy Protocol是 Haproxy 实现的一个四层源地址保留方案。它的原理特别简单,Proxy 在与后端 Server 建立 TCP 连接后,在发送实际应用数据之前,首先发送一个Proxy Protocol协议头(包括客户端源 IP/端口、目标IP/端口等信息)。这样,后端 server 通过解析协议头获取真实的客户端源 IP 地址。

Proxy Protocol需要 Proxy 和 Server 同时支持该协议。但它却可以实现跨多层中间代理保持源 IP。这有点类似七层 XFF 的设计思想。

istio 中实现源 IP 保持

istio 中,由于 istio ingressgateway 以及 sidecar 的存在,应用要获取客户端源 IP 地址,会变得比较困难。但 Envoy 本身为了支持透明代理,它支持Proxy Protocol,再结合 TPROXY,我们可以在 istio 的服务中获取到源 IP。

东西向流量

istio 东西向服务访问时,由于 Sidecar 的注入,所有进出服务的流量均被 Envoy 拦截代理,然后再由 Envoy 将请求转给应用。所以,应用收到的请求的源地址,是 Envoy 访问过来的地址127.0.0.6

# kubectl -n foo apply -f samples/httpbin/httpbin.yaml
# kubectl -n foo apply -f samples/sleep/sleep.yaml
# kubectl -n foo get pods -o wide
NAME                       READY   STATUS    RESTARTS   AGE    IP            NODE           NOMINATED NODE   READINESS GATES
httpbin-74fb669cc6-qvlb5   2/2     Running   0          4m9s   172.17.0.57   10.206.2.144              
sleep-74b7c4c84c-9nbtr     2/2     Running   0          6s     172.17.0.58   10.206.2.144              


# kubectl -n foo exec -it deploy/sleep -c sleep -- curl http://httpbin:8000/ip
{
  "origin": "127.0.0.6"
}

可以看到,httpbin 看到的源 IP 是127.0.0.6。从 socket 信息,也可以确认这一点。

# kubectl -n foo exec -it deploy/httpbin -c httpbin -- netstat -ntp | grep 80
tcp        0      0 172.17.0.57:80          127.0.0.6:56043         TIME_WAIT   -
  • istio 开启 TPROXY

我们修改 httpbin deployment,使用 TPROXY(注意httpbin的 IP 变成了172.17.0.59):

# kubectl patch deployment -n foo httpbin -p '{"spec":{"template":{"metadata":{"annotations":{"sidecar.istio.io/interceptionMode":"TPROXY"}}}}}'
# kubectl -n foo get pods -l app=httpbin  -o wide
NAME                       READY   STATUS    RESTARTS   AGE   IP            NODE           NOMINATED NODE   READINESS GATES
httpbin-6565f59ff8-plnn7   2/2     Running   0          43m   172.17.0.59   10.206.2.144              

# kubectl -n foo exec -it deploy/sleep -c sleep -- curl http://httpbin:8000/ip
{
  "origin": "172.17.0.58"
}

可以看到,httpbin 可以得到 sleep 端的真实 IP。

socket 的状态:

# kubectl -n foo exec -it deploy/httpbin -c httpbin -- netstat -ntp | grep 80                  
tcp        0      0 172.17.0.59:80          172.17.0.58:35899       ESTABLISHED 9/python3           
tcp        0      0 172.17.0.58:35899       172.17.0.59:80          ESTABLISHED -

第一行是 httpbin 的接收端 socket,第二行是 envoy 的发送端 socket。

httpbin envoy日志:

{"bytes_received":0,"upstream_local_address":"172.17.0.58:35899",
"downstream_remote_address":"172.17.0.58:46864","x_forwarded_for":null,
"path":"/ip","istio_policy_status":null,
"response_code":200,"upstream_service_time":"1",
"authority":"httpbin:8000","start_time":"2022-05-30T02:09:13.892Z",
"downstream_local_address":"172.17.0.59:80","user_agent":"curl/7.81.0-DEV","response_flags":"-",
"upstream_transport_failure_reason":null,"request_id":"2b2ab6cc-78da-95c0-b278-5b3e30b514a0",
"protocol":"HTTP/1.1","requested_server_name":null,"duration":1,"bytes_sent":30,"route_name":"default",
"upstream_cluster":"inbound|80||","upstream_host":"172.17.0.59:80","method":"GET"}

可以看到,

  • downstream_remote_address: 172.17.0.58:46864 ## sleep的地址
  • downstream_local_address: 172.17.0.59:80 ## sleep访问的目标地址
  • upstream_local_address: 172.17.0.58:35899 ## httpbin envoy连接httpbin的local address(为sleep的IP)
  • upstream_host: 172.17.0.59:80 ## httpbin envoy访问的目标地址

httpbin envoy 连接 httpbin 的 local address 为 sleep 的 IP 地址。

南北向流量

对于南北向流量,客户端先请求 CLB,CLB 将请求转给 ingressgateway,再转到后端服务,由于中间多了 ingressgateway 一跳,想要获取客户端源 IP,变得更加困难。

我们以 TCP 协议访问 httpbin:

apiVersion: v1
kind: Service
metadata:
  name: httpbin
  namespace: foo
  labels:
    app: httpbin
    service: httpbin
spec:
  ports:
  - name: tcp
    port: 8000
    targetPort: 80
  selector:
    app: httpbin
---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: httpbin-gw
  namespace: foo
spec:
  selector:
    istio: ingressgateway # use istio default controller
  servers:
  - port:
      number: 8000
      name: tcp
      protocol: TCP
    hosts:
    - "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin
  namespace: foo
spec:
  hosts:
    - "*"
  gateways:
    - httpbin-gw
  tcp:
    - match:
      - port: 8000
      route:
        - destination:
            port:
              number: 8000
            host: httpbin

通过 ingressgateway 访问 httpbin:

# export GATEWAY_URL=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
# curl http://$GATEWAY_URL:8000/ip
{
  "origin": "172.17.0.54"
}

可以看到,httpbin 看到的地址是ingressgateway的地址:

# kubectl -n istio-system get pods -l istio=ingressgateway -o wide
NAME                                    READY   STATUS    RESTARTS   AGE     IP            NODE           NOMINATED NODE   READINESS GATES
istio-ingressgateway-5d5b776b7b-pxc2g   1/1     Running   0          3d15h   172.17.0.54   10.206.2.144              

虽然我们在httpbin envoy开启了透明代理,但 ingressgateway 并不能把 client 的源地址传到httpbin envoy。基于 envoy 实现的Proxy Protocol,可以解决这个问题。

通过 EnvoyFilter 在 ingressgateway 和 httpbin 同时开启Proxy Protocol支持。

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: ingressgw-pp
  namespace: istio-system
spec:
  configPatches:
  - applyTo: CLUSTER
    patch:
      operation: MERGE
      value:
        transport_socket:
          name: envoy.transport_sockets.upstream_proxy_protocol
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.transport_sockets.proxy_protocol.v3.ProxyProtocolUpstreamTransport
            config:
              version: V1
            transport_socket:
              name: "envoy.transport_sockets.raw_buffer"
  workloadSelector:
    labels:
      istio: ingressgateway
---
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: httpbin-pp
  namespace: foo
spec:
  configPatches:
  - applyTo: LISTENER
    match:
      context: SIDECAR_INBOUND
    patch:
      operation: MERGE
      value:
        listener_filters:
        - name: envoy.filters.listener.proxy_protocol
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.listener.proxy_protocol.v3.ProxyProtocol
        - name: envoy.filters.listener.original_dst
        - name: envoy.filters.listener.original_src
  workloadSelector:
    labels:
      app: httpbin

再次通过 LB 访问 httpbin:

# curl http://$GATEWAY_URL:8000/ip
{
  "origin": "106.52.131.116"
}

httpbin 得到了客户端的源 IP。

  • ingressgateway envoy 日志
{"istio_policy_status":null,"protocol":null,"bytes_sent":262,"downstream_remote_address":"106.52.131.116:6093","start_time":"2022-05-30T03:33:33.759Z",
"upstream_service_time":null,"authority":null,"requested_server_name":null,"user_agent":null,"request_id":null,
"upstream_cluster":"outbound|8000||httpbin.foo.svc.cluster.local","upstream_transport_failure_reason":null,"duration":37,"response_code":0,
"method":null,"downstream_local_address":"172.17.0.54:8000","route_name":null,"upstream_host":"172.17.0.59:80","bytes_received":83,"path":null,
"x_forwarded_for":null,"upstream_local_address":"172.17.0.54:36162","response_flags":"-"}

可以看到,

  • downstream_remote_address: 106.52.131.116:6093 ## 客户端源地址
  • downstream_local_address: 172.17.0.54:8000
  • upstream_local_address: 172.17.0.54:42122 ## ingressgw local addr
  • upstream_host: 172.17.0.59:80 ## httpbin 地址
  • httpbin envoy日志
{"istio_policy_status":null,"response_flags":"-","protocol":null,"method":null,"upstream_transport_failure_reason":null,"authority":null,"duration":37,
"x_forwarded_for":null,"user_agent":null,"downstream_remote_address":"106.52.131.116:6093","downstream_local_address":"172.17.0.59:80",
"bytes_sent":262,"path":null,"requested_server_name":null,"upstream_service_time":null,"request_id":null,"bytes_received":83,"route_name":null,
"upstream_local_address":"106.52.131.116:34431","upstream_host":"172.17.0.59:80","response_code":0,"start_time":"2022-05-30T03:33:33.759Z","upstream_cluster":"inbound|80||"}

可以看到,

  • downstream_remote_address: 106.52.131.116:6093 ## 客户端源地址
  • downstream_local_address: 172.17.0.59:80 ## httpbin地址
  • upstream_local_address: 106.52.131.116:34431 ## 保留了客户端IP,port不一样
  • upstream_host: 172.17.0.59:80 ## httpbin地址

值得注意的是,httpbin envoyupstream_local_address保留了客户端的 IP,这样,httpbin 看到的源地址 IP,就是客户端的真实 IP。

  • 数据流

相关实现分析

TRPOXY

TPROXY 的内核实现参考net/netfilter/xt_TPROXY.c。

istio-iptables会设置下面的 iptables 规则,给数据报文设置标记。

-A PREROUTING -p tcp -j ISTIO_INBOUND
-A PREROUTING -p tcp -m mark --mark 0x539 -j CONNMARK --save-mark --nfmask 0xffffffff --ctmask 0xffffffff
-A OUTPUT -p tcp -m connmark --mark 0x539 -j CONNMARK --restore-mark --nfmask 0xffffffff --ctmask 0xffffffff
-A ISTIO_DIVERT -j MARK --set-xmark 0x539/0xffffffff
-A ISTIO_DIVERT -j ACCEPT
-A ISTIO_INBOUND -p tcp -m conntrack --ctstate RELATED,ESTABLISHED -j ISTIO_DIVERT
-A ISTIO_INBOUND -p tcp -j ISTIO_TPROXY
-A ISTIO_TPROXY ! -d 127.0.0.1/32 -p tcp -j TPROXY --on-port 15006 --on-ip 0.0.0.0 --tproxy-mark 0x539/0xffffffff

值得一提的是,TPROXY 不用依赖 NAT,本身就可以实现数据包的重定向。另外,结合策略路由,将非本地的数据包通过本地 lo 路由:

# ip rule list
0:	from all lookup local 
32765:	from all fwmark 0x539 lookup 133 
32766:	from all lookup main 
32767:	from all lookup default 

# ip route show table 133
local default dev lo scope host

TPROXY 的更多详细介绍参考这里。

Envoy 中 Proxy Protocol 的实现

  • proxy protocol header format

这里使用了Version 1(Human-readable header format),如下:

0000   50 52 4f 58 59 20 54 43 50 34 20 31 30 36 2e 35   PROXY TCP4 106.5
0010   32 2e 31 33 31 2e 31 31 36 20 31 37 32 2e 31 37   2.131.116 172.17
0020   2e 30 2e 35 34 20 36 30 39 33 20 38 30 30 30 0d   .0.54 6093 8000.
0030   0a                                                .

可以看到,header 包括 client 和 ingressgateway 的IP:PORT信息。更加详细的介绍参考这里。

  • ProxyProtocolUpstreamTransport

ingressgateway 作为发送端,使用
ProxyProtocolUpstreamTransport
,构建Proxy Protocol头部:

/// source/extensions/transport_sockets/proxy_protocol/proxy_protocol.cc

void UpstreamProxyProtocolSocket::generateHeaderV1() {
  // Default to local addresses (used if no downstream connection exists e.g. health checks)
  auto src_addr = callbacks_->connection().addressProvider().localAddress(); 
  auto dst_addr = callbacks_->connection().addressProvider().remoteAddress();

  if (options_ && options_->proxyProtocolOptions().has_value()) {
    const auto options = options_->proxyProtocolOptions().value();
    src_addr = options.src_addr_;
    dst_addr = options.dst_addr_;
  }

  Common::ProxyProtocol::generateV1Header(*src_addr->ip(), *dst_addr->ip(), header_buffer_);
}
  • envoy.filters.listener.proxy_protocol

httpbin envoy作为接收端,配置ListenerFilter(
envoy.filters.listener.proxy_protocol
)解析Proxy Protocol头部:

/// source/extensions/filters/listener/proxy_protocol/proxy_protocol.cc

ReadOrParseState Filter::onReadWorker() {
  Network::ConnectionSocket& socket = cb_->socket(); /// ConnectionHandlerImpl::ActiveTcpSocket
...
  if (proxy_protocol_header_.has_value() && !proxy_protocol_header_.value().local_command_) {
...
    // Only set the local address if it really changed, and mark it as address being restored.
    if (*proxy_protocol_header_.value().local_address_ !=
        *socket.addressProvider().localAddress()) { /// proxy protocol header: 172.17.0.54:8000
      socket.addressProvider().restoreLocalAddress(proxy_protocol_header_.value().local_address_); /// => 172.17.0.54:8000
    } /// Network::ConnectionSocket
    socket.addressProvider().setRemoteAddress(proxy_protocol_header_.value().remote_address_); /// 修改downstream_remote_address为106.52.131.116
  }

  // Release the file event so that we do not interfere with the connection read events.
  socket.ioHandle().resetFileEvents();
  cb_->continueFilterChain(true); /// ConnectionHandlerImpl::ActiveTcpSocket
  return ReadOrParseState::Done;
}

这里值得注意的,
envoy.filters.listener.proxy_protocol
在解析proxy protocol header时,local_address为发送端的dst_addr(172.17.0.54:8000)remote_address为发送端的src_addr(106.52.131.116)。顺序刚好反过来了。

经过proxy_protocol的处理,连接的downstream_remote_address被修改为client的源地址。

  • envoy.filters.listener.original_src

对于
sidecar.istio.io/interceptionMode: TPROXY
virtualInbound listener会增加
envoy.filters.listener.original_src
:

# istioctl -n foo pc listeners deploy/httpbin --port 15006 -o json
[
    {
        "name": "virtualInbound",
        "address": {
            "socketAddress": {
                "address": "0.0.0.0",
                "portValue": 15006
            }
        },
        "filterChains": [...],
        "listenerFilters": [
            {
                "name": "envoy.filters.listener.original_dst",
                "typedConfig": {
                    "@type": "type.googleapis.com/envoy.extensions.filters.listener.original_dst.v3.OriginalDst"
                }
            },
            {
                "name": "envoy.filters.listener.original_src",
                "typedConfig": {
                    "@type": "type.googleapis.com/envoy.extensions.filters.listener.original_src.v3.OriginalSrc",
                    "mark": 1337
                }
            }
        ...
        ]
        "listenerFiltersTimeout": "0s",
        "continueOnListenerFiltersTimeout": true,
        "transparent": true,
        "trafficDirection": "INBOUND",
        "accessLog": [...]
    }
]


envoy.filters.listener.original_src
通过tcp option实现修改upstream_local_addressdownstream_remote_address,实现透传client IP。

/// source/extensions/filters/listener/original_src/original_src.cc

Network::FilterStatus OriginalSrcFilter::onAccept(Network::ListenerFilterCallbacks& cb) {
  auto& socket = cb.socket(); /// ConnectionHandlerImpl::ActiveTcpSocket.socket()
  auto address = socket.addressProvider().remoteAddress();   /// get downstream_remote_address
  ASSERT(address);

  ENVOY_LOG(debug,
            "Got a new connection in the original_src filter for address {}. Marking with {}",
            address->asString(), config_.mark());

...
  auto options_to_add =
      Filters::Common::OriginalSrc::buildOriginalSrcOptions(std::move(address), config_.mark()); 
  socket.addOptions(std::move(options_to_add)); /// Network::Socket::Options
  return Network::FilterStatus::Continue;
}
  • envoy.filters.listener.original_dst

另外,httbin envoy作为 ingressgateway 的接收端,virtualInbound listener还配置了 ListenerFilter(
envoy.filters.listener.original_dst
),来看看它的作用。

// source/extensions/filters/listener/original_dst/original_dst.cc

Network::FilterStatus OriginalDstFilter::onAccept(Network::ListenerFilterCallbacks& cb) {
  ENVOY_LOG(debug, "original_dst: New connection accepted");
  Network::ConnectionSocket& socket = cb.socket();

  if (socket.addressType() == Network::Address::Type::Ip) { /// socket SO_ORIGINAL_DST option
    Network::Address::InstanceConstSharedPtr original_local_address = getOriginalDst(socket); /// origin dst address

    // A listener that has the use_original_dst flag set to true can still receive
    // connections that are NOT redirected using iptables. If a connection was not redirected,
    // the address returned by getOriginalDst() matches the local address of the new socket.
    // In this case the listener handles the connection directly and does not hand it off.
    if (original_local_address) { /// change local address to origin dst address
      // Restore the local address to the original one.
      socket.addressProvider().restoreLocalAddress(original_local_address);
    }
  }

  return Network::FilterStatus::Continue;
}

对于 istio,由 iptable 截持原有 request,并转到15006(in request),或者15001(out request)端口,所以,处理 request 的 socket 的local address,并不请求的original dst addressoriginal_dst ListenerFilter负责将 socket 的 local address 改为original dst address

对于virtualOutbound listener,不会直接添加
envoy.filters.listener.original_dst
,而是将use_original_dst设置为 true,然后 envoy 会自动添加
envoy.filters.listener.original_dst
。同时,virtualOutbound listener会将请求,转给请求原目的地址关联的 listener 进行处理。

对于virtualInbound listener,会直接添加
envoy.filters.listener.original_dst
。与virtualOutbound listener不同的是,它只是将地址改为original dst address,而不会将请求转给对应的 listener 处理(对于入请求,并不存在 dst address 的 listener)。实际上,对于入请求是由 FilterChain 完成处理。

参考 istio 生成virtualInbound listener的代码:

// istio/istio/pilot/pkg/networking/core/v1alpha3/listener_builder.go

func (lb *ListenerBuilder) aggregateVirtualInboundListener(passthroughInspectors map[int]enabledInspector) *ListenerBuilder {
	// Deprecated by envoyproxy. Replaced
	// 1. filter chains in this listener
	// 2. explicit original_dst listener filter
	// UseOriginalDst: proto.BoolTrue,
	lb.virtualInboundListener.UseOriginalDst = nil
	lb.virtualInboundListener.ListenerFilters = append(lb.virtualInboundListener.ListenerFilters,
		xdsfilters.OriginalDestination, /// 添加envoy.filters.listener.original_dst
	)
	if lb.node.GetInterceptionMode() == model.InterceptionTproxy { /// TPROXY mode
		lb.virtualInboundListener.ListenerFilters =
			append(lb.virtualInboundListener.ListenerFilters, xdsfilters.OriginalSrc)
	}
...

小结

基于 TPROXY 以及 Proxy Protocol,我们可以在 istio 中,实现四层协议的客户端源 IP 的保持。

文章来自
https://www.cnblogs.com/tencent-cloud-native/p/16355019.html

相关推荐

4万多吨豪华游轮遇险 竟是因为这个原因……

(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...

“菜鸟黑客”必用兵器之“渗透测试篇二”

"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...

科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白

作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...

麦子陪你做作业(二):KEGG通路数据库的正确打开姿势

作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...

知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势

智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...

每日新闻播报(September 14)_每日新闻播报英文

AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...

香港新巴城巴开放实时到站数据 供科技界研发使用

中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...

5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper

本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...

Qt动画效果展示_qt显示图片

今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...

如何从0到1设计实现一门自己的脚本语言

作者:dong...

三年级语文上册 仿写句子 需要的直接下载打印吧

描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...

C++|那些一看就很简洁、优雅、经典的小代码段

目录0等概率随机洗牌:1大小写转换2字符串复制...

二年级上册语文必考句子仿写,家长打印,孩子照着练

二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...

一年级语文上 句子专项练习(可打印)

...

亲自上阵!C++ 大佬深度“剧透”:C++26 将如何在代码生成上对抗 Rust?

...

取消回复欢迎 发表评论: