我始终觉得,天生的出身很重要,但后天的努力更加重要,所以如今的很多“科班”往往不如后天努力的“非科班”。所以,我们需要重新给“专业”和“专家”下一个定义:所谓专业,就是别人不搞你搞,这就是你的“专业”;你和别人同时搞,你比别人搞的好,就是“专家”。
说到http协议和http请求,很多人都知道,但是他们真的“知道”吗?我面试过很多求职者,一说到http协议,他们能滔滔不绝,然后我问他http协议的具体格式是啥样子的?很多人不清楚,不清楚就不清楚吧,他甚至能将http协议的头扯到html文档头部。当我问http GET和POST请求的时候,GET请求是什么形式一般人都可以答出来,但是POST请求的数据放在哪里,服务器如何识别和解析这些POST数据,很多人又说不清道不明了。当说到http服务器时,很多人离开了apache、Nginx这样现成的http server之外,自己实现一个http服务器无从下手,如果实际应用场景有需要使用到一些简单http请求时,使用apache、Nginx这样重量级的http服务器程序实在劳师动众,你可以尝试自己实现一个简单的。
上面提到的问题,如果您不能清晰地回答出来,可以阅读一下这篇文章,这篇文章在不仅介绍http的格式,同时带领大家从零实现一个简单的http服务器程序。
最近很多朋友希望我的flamingo服务器支持http协议,我自己也想做一个微信小程序,小程序通过http协议连接通过我的flamingo服务器进行聊天。flamingo是一个开源的即时通讯软件,目前除了服务器端,还有pc端、android端,后面会支持更多的终端。关于flamingo的介绍您可以参考这里:[(七)开源一款即时通讯软件的源码] ,这是我不断维护一个项目,其最新代码下载地址是:https://github.com/baloonwj/flamingo,更新日志:https://github.com/baloonwj/flamingo/issues/1。下面是flamingo的部分截图:
1. http协议是应用层协议,一般建立在tcp协议的基础之上(当然你的实现非要基于udp也是可以的),也就是说http协议的数据收发是通过tcp协议的。
2. http协议也分为head和body两部分,但是我们一般说的html中的和标记不是html协议的头和身体,它们都是html协议的body部分。
那么http协议的头到底长啥样子呢?我们来介绍一下http协议吧。
http协议的格式如下:
1GET或POST 请求的url路径(一般是去掉域名的路径) HTTP协议版本号\r\n
2字段1名: 字段1值\r\n
3字段2名: 字段2值\r\n
4 …
5字段n名 : 字段n值\r\n
6\r\n
7http协议包体内容
也就是说http协议由两部分组成:包头和包体,包头与包体之间使用一个\r\n分割,由于http协议包头的每一行都是以\r\n结束,所以http协议包头一般以\r\n\r\n结束。
举个例子,比如我们在浏览器中请求http://www.hootina.org/index_2013.php这个网址,这是一个典型的GET方法,浏览器组装的http数据包格式如下:
1GET /index_2013.php HTTP/1.1\r\n
2Host: www.hootina.org\r\n
3Connection: keep-alive\r\n
4Upgrade-Insecure-Requests: 1\r\n
5User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n
6Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\n
7Accept-Encoding: gzip, deflate\r\n
8Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n
9\r\n
上面这个请求只有包头没有包体,http协议的包头不是必须的,也就是说GET请求一般没有包体。
如果GET请求带参数,那么一般是附加在请求的url后面,参数与参数之间使用&分割,例如请求http://www.hootina.org/index_2013.php?param1=value1¶m2=value2¶m3=value3,我们看下这个请求组装的的http协议包格式:
1GET /index_2013.php?param1=value1¶m2=value2¶m3=value3 HTTP/1.1\r\n
2Host: www.hootina.org\r\n
3Connection: keep-alive\r\n
4Upgrade-Insecure-Requests: 1\r\n
5User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n
6Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\n
7Accept-Encoding: gzip, deflate\r\n
8Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n
9\r\n
对比一下,你现在知道http协议的GET参数放在协议包的什么位置了吧。
那么POST的数据放在什么位置呢?我们再12306网站https://kyfw.12306.cn/otn/login/init中登陆输入用户名和密码:
然后发现浏览器以POST方式组装了http协议包发送了我们的用户名、密码和其他一些信息,组装的包格式如下:
1POST /passport/web/login HTTP/1.1\r\n
2Host: kyfw.12306.cn\r\n
3Connection: keep-alive\r\n
4Content-Length: 55\r\n
5Accept: application/json, text/javascript, */*; q=0.01\r\n
6Origin: https://kyfw.12306.cn\r\n
7X-Requested-With: XMLHttpRequest\r\n
8User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n
9Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n
10Referer: https://kyfw.12306.cn/otn/login/init\r\n
11Accept-Encoding: gzip, deflate, br\r\n
12Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n
13Cookie: _passport_session=0b2cc5b86eb74bcc976bfa9dfef3e8a20712; _passport_ct=18d19b0930954d76b8057c732ce4cdcat8137; route=6f50b51faa11b987e576cdb301e545c4; RAIL_EXPIRATION=1526718782244; RAIL_DEVICEID=QuRAhOyIWv9lwWEhkq03x5Yl_livKZxx7gW6_-52oTZQda1c4zmVWxdw5Zk79xSDFHe9LJ57F8luYOFp_yahxDXQAOmEV8U1VgXavacuM2UPCFy3knfn42yTsJM3EYOy-hwpsP-jTb2OXevJj5acf40XsvsPDcM7; BIGipServerpool_passport=300745226.50215.0000; BIGipServerotn=1257243146.38945.0000; BIGipServerpassport=1005060362.50215.0000\r\n
14\r\n
15username=balloonwj%40qq.com&password=iloveyou&appid=otn
其中username=balloonwj%40qq.com&password=iloveyou&appid=otn
就是我们的POST数据,但是大家需要注意的以下几种,不要搞错:
1. 我的用户名是balloonwj@qq.com,到POST里面变成balloonwj%40qq.com,其中%40是@符号的16进制转码形式。这个码表可以参考这里:**http://www.w3school.com.cn/tags/html_ref_urlencode.html**
2.这里有三个变量,分别是username、password和appid,他们之间使用&符号分割,但是请注意的是,这不意味着传递多个POST变量时必须使用&符号分割,只不过这里是浏览器html表单(输入用户名和密码的文本框是html表单的一种)分割多个变量采用的默认方式而已。你可以根据你的需求,来自由定制,只要让服务器知道你的解析方式即可。比如可以这么分割:
方法一
1username=balloonwj%40qq.com|password=iloveyou|appid=otn
方法二
1username:balloonwj%40qq.com\r\n
2password:iloveyou\r\n
3appid:otn\r\n
方法三
1username,password,appid=balloonwj%40qq.com,iloveyou,otn
不管怎么分割,只要你能自己按一定的规则解析出来就可以了。
不知道你注意到没有,上面的POST数据放在http包体中,服务器如何解析呢?可能你没明白我的意思,看下图:
如上图所示,由于http协议是基于tcp协议的,tcp协议是流式协议,包头部分可以通过多出的\r\n来分界,包体部分如何分界呢?这是协议本身要解决的问题。目前一般有两种方式,第一种方式就是在包头中有个content-Length
字段,这个字段的值的大小标识了POST数据的长度,上图中55就是数据username=balloonwj%40qq.com&password=iloveyou&appid=otn
的长度,服务器收到一个数据包后,先从包头解析出这个字段的值,再根据这个值去读取相应长度的作为http协议的包体数据。还有一个格式叫做http chunked技术(分块),大致意思是将大包分成小包,具体的详情有兴趣的读者可以自行搜索学习。
如果您能掌握以上说的http协议,你就可以自己通过代码组装http协议发送http请求了(也是各种开源http库的做法)。我们先简单地介绍一下如何模拟发送http。举个例子,我们要请求http://www.hootina.org/index_2013.php,那么我们可以先通过域名得到ip地址,即通过socket API gethostbyname()
得到www.hootina.org的ip地址,由于http服务器默认的端口号是80,有了域名和ip地址之后,我们使用socket API connect()
去连接服务器,然后根据上面介绍的格式组装成http协议包,利用socket API send()
函数发出去,如果服务器有应答,我们可以使用socket API recv()
去接受数据,接下来就是解析数据(先解析包头和包体)。
我们这里简化一些问题,假设客户端发送的请求都是GET请求,当客户端发来http请求之后,我们拿到http包后就做相应的处理。我们以为我们的flamingo服务器实现一个支持http格式的注册请求为例。假设用户在浏览器里面输入以下网址,就可以实现一个注册功能:
http://120.55.94.78:12345/register.do?p={"username": "13917043329", "nickname": "balloon", "password": "123"}
这里我们的http协议使用的是12345端口号而不是默认的80端口。如何侦听12345端口,这个是非常基础的知识了,这里就不介绍了。当我们收到数据以后:
1void HttpSession::OnRead(const std::shared_ptr<TcpConnection>& conn, Buffer* pBuffer, Timestamp receivTime)
2{
3 //LOG_INFO << "Recv a http request from " << conn->peerAddress().toIpPort();
4
5 string inbuf;
6 //先把所有数据都取出来
7 inbuf.append(pBuffer->peek(), pBuffer->readableBytes());
8 //因为一个http包头的数据至少\r\n\r\n,所以大于4个字符
9 //小于等于4个字符,说明数据未收完,退出,等待网络底层接着收取
10 if (inbuf.length() <= 4)
11 return;
12
13 //我们收到的GET请求数据包一般格式如下:
14 /*
15 GET /register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22} HTTP/1.1\r\n
16 Host: 120.55.94.78:12345\r\n
17 Connection: keep-alive\r\n
18 Upgrade-Insecure-Requests: 1\r\n
19 User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n
20 Accept-Encoding: gzip, deflate\r\n
21 Accept-Language: zh-CN, zh; q=0.9, en; q=0.8\r\n
22 \r\n
23 */
24 //检查是否以\r\n\r\n结束,如果不是说明包头不完整,退出
25 string end = inbuf.substr(inbuf.length() - 4);
26 if (end != "\r\n\r\n")
27 return;
28
29 //以\r\n分割每一行
30 std::vector<string> lines;
31 StringUtil::Split(inbuf, lines, "\r\n");
32 if (lines.size() < 1 || lines[0].empty())
33 {
34 conn->forceClose();
35 return;
36 }
37
38 std::vector<string> chunk;
39 StringUtil::Split(lines[0], chunk, " ");
40 //chunk中至少有三个字符串:GET+url+HTTP版本号
41 if (chunk.size() < 3)
42 {
43 conn->forceClose();
44 return;
45 }
46
47 LOG_INFO << "url: " << chunk[1] << " from " << conn->peerAddress().toIpPort();
48 //inbuf = /register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22}
49 std::vector<string> part;
50 //通过?分割成前后两端,前面是url,后面是参数
51 StringUtil::Split(chunk[1], part, "?");
52 //chunk中至少有三个字符串:GET+url+HTTP版本号
53 if (part.size() < 2)
54 {
55 conn->forceClose();
56 return;
57 }
58
59 string url = part[0];
60 string param = part[1].substr(2);
61
62 if (!Process(conn, url, param))
63 {
64 LOG_ERROR << "handle http request error, from:" << conn->peerAddress().toIpPort() << ", request: " << pBuffer->retrieveAllAsString();
65 }
66
67 //短连接,处理完关闭连接
68 conn->forceClose();
69}
代码注释都写的很清楚,我们先利用\r\n分割得到每一行,其中第一行的数据是:
1GET /register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22} HTTP/1.1
其中%22是双引号的url转码形式,%20是空格的url转码形式,然后我们根据空格分成三段,其中第二段就是我们的网址和参数:
1/register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22}
然后我们根据网址与参数之间的问号将这个分成两段:第一段是网址,第二段是参数:
1bool HttpSession::Process(const std::shared_ptr<TcpConnection>& conn, const std::string& url, const std::string& param)
2{
3 if (url.empty())
4 return false;
5
6 if (url == "/register.do")
7 {
8 OnRegisterResponse(param, conn);
9 }
10 else if (url == "/login.do")
11 {
12 OnLoginResponse(param, conn);
13 }
14 else if (url == "/getfriendlist.do")
15 {
16
17 }
18 else if (url == "/getgroupmembers.do")
19 {
20
21 }
22 else
23 return false;
24
25
26 return true;
27}
然后我们根据url匹配网址,如果是注册请求,会走注册处理逻辑:
1void HttpSession::OnRegisterResponse(const std::string& data, const std::shared_ptr<TcpConnection>& conn)
2{
3 string retData;
4 string decodeData;
5 URLEncodeUtil::Decode(data, decodeData);
6 BussinessLogic::RegisterUser(decodeData, conn, false, retData);
7 if (!retData.empty())
8 {
9 std::string response;
10 URLEncodeUtil::Encode(retData, response);
11 MakeupResponse(retData, response);
12 conn->send(response);
13
14 LOG_INFO << "Response to client: cmd=msg_type_register" << ", data=" << retData << conn->peerAddress().toIpPort();;
15 }
16}
注册结果放在retData中,为了发给客户端,我们将结果中的特殊字符如双引号转码,如返回结果是:
1{"code":0, "msg":"ok"}
会被转码成:
1{%22code%22:0,%20%22msg%22:%22ok%22}
然后,将数据组装成http协议发给客户端,给客户端的应答协议与http请求协议有一点点差别,就是将请求的url路径换成所谓的http响应码,如200表示应答正常返回、404页面不存在。应答协议格式如下:
1GET或POST 响应码 HTTP协议版本号\r\n
2字段1名: 字段1值\r\n
3字段2名: 字段2值\r\n
4 …
5字段n名 : 字段n值\r\n
6\r\n
7http协议包体内容
举个例子如:
1HTTP/1.1 200 OK\r\n
2Content-Type: text/html\r\n
3Content-Length:42\r\n
4\r\n
5{%22code%22:%200,%20%22msg%22:%20%22ok%22}
注意,包头中的Content-Length长度必须正好是包体{%22code%22:%200,%20%22msg%22:%20%22ok%22}
的长度,这里是42。这也符合我们浏览器的返回结果:
当然,需要注意的是,我们一般说http连接一般是短连接,这里我们也实现了这个功能(看上面的代码:conn->forceClose();
),不管一个http请求是否成功,服务器处理后立马就关闭连接。
当然,这里还有一些没处理好的地方,如果你仔细观察上面的代码就会发现这个问题,就是不满足一个http包头时的处理,如果某个客户端(不是使用浏览器)通过程序模拟了一个连接请求,但是迟迟不发含有\r\n\r\n的数据,这路连接将会一直占用。我们可以判断收到的数据长度,防止别有用心的客户端给我们的服务器乱发数据。我们假定,我们能处理的最大url长度是2048,如果用户发送的数据累积不含\r\n\r\n,且超过2048个,我们认为连接非法,将连接断开。代码修改成如下形式:
1void HttpSession::OnRead(const std::shared_ptr<TcpConnection>& conn, Buffer* pBuffer, Timestamp receivTime)
2{
3 //LOG_INFO << "Recv a http request from " << conn->peerAddress().toIpPort();
4
5 string inbuf;
6 //先把所有数据都取出来
7 inbuf.append(pBuffer->peek(), pBuffer->readableBytes());
8 //因为一个http包头的数据至少\r\n\r\n,所以大于4个字符
9 //小于等于4个字符,说明数据未收完,退出,等待网络底层接着收取
10 if (inbuf.length() <= 4)
11 return;
12
13 //我们收到的GET请求数据包一般格式如下:
14 /*
15 GET /register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22} HTTP/1.1\r\n
16 Host: 120.55.94.78:12345\r\n
17 Connection: keep-alive\r\n
18 Upgrade-Insecure-Requests: 1\r\n
19 User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n
20 Accept-Encoding: gzip, deflate\r\n
21 Accept-Language: zh-CN, zh; q=0.9, en; q=0.8\r\n
22 \r\n
23 */
24 //检查是否以\r\n\r\n结束,如果不是说明包头不完整,退出
25 string end = inbuf.substr(inbuf.length() - 4);
26 if (end != "\r\n\r\n")
27 return;
28 //超过2048个字符,且不含\r\n\r\n,我们认为是非法请求
29 else if (inbuf.length() >= MAX_URL_LENGTH)
30 {
31 conn->forceClose();
32 return;
33 }
34
35 //以\r\n分割每一行
36 std::vector<string> lines;
37 StringUtil::Split(inbuf, lines, "\r\n");
38 if (lines.size() < 1 || lines[0].empty())
39 {
40 conn->forceClose();
41 return;
42 }
43
44 std::vector<string> chunk;
45 StringUtil::Split(lines[0], chunk, " ");
46 //chunk中至少有三个字符串:GET+url+HTTP版本号
47 if (chunk.size() < 3)
48 {
49 conn->forceClose();
50 return;
51 }
52
53 LOG_INFO << "url: " << chunk[1] << " from " << conn->peerAddress().toIpPort();
54 //inbuf = /register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22}
55 std::vector<string> part;
56 //通过?分割成前后两端,前面是url,后面是参数
57 StringUtil::Split(chunk[1], part, "?");
58 //chunk中至少有三个字符串:GET+url+HTTP版本号
59 if (part.size() < 2)
60 {
61 conn->forceClose();
62 return;
63 }
64
65 string url = part[0];
66 string param = part[1].substr(2);
67
68 if (!Process(conn, url, param))
69 {
70 LOG_ERROR << "handle http request error, from:" << conn->peerAddress().toIpPort() << ", request: " << pBuffer->retrieveAllAsString();
71 }
72
73 //短连接,处理完关闭连接
74 conn->forceClose();
75}
但这只能解决发送非法数据的情况,如果一个客户端连上来不给我们发任何数据,这段逻辑就无能为力了。如果不断有客户端这么做,会浪费我们大量的连接资源,所以我们还需要一个定时器去定时检测哪些http连接超过一定时间内没给我们发数据,找到后将连接断开。这又涉及到服务器定时器如何设计了,关于这部分请参考我写的其他文章。
全文完。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/MYxZ0IJaNHIKVBwkY7tDWA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。