使用 WebSocket 实现一个网页版的聊天室(摸鱼更隐蔽)

发表于 2年以前  | 总阅读数:366 次

WebSocket简介

WebSocket协议是完全重新设计的协议,旨在为Web上的双向数据传输问题提供一个切实可行的解决方案,使得客户端和服务器之间可以在任意时刻传输消息,因此,这也就要求它们异步地处理消息回执

WebSocket特点:

  • HTML5 中的协议,实现与客户端与服务器双向,基于消息的文本或二进制数据通信
  • 适合于对数据的实时性要求比较强的场景,如通信、直播、共享桌面,特别适合于客户端与服务端频繁交互的情况下,如实时共享、多人协作等平台
  • 采用新的协议,后端需要单独实现
  • 客户端并不是所有浏览器都支持

WebSocket通信握手

在从标准的 HTTP 或者 HTTPS协议切换到WebSocket时,将会使用一种称为握手的机制 ,因此,使用WebSocket的应用程序将始终以HTTP/S作为开始,然后再执行升级。这个升级动作发生的确切时刻特定于应用程序;它可能会发生在启动时,也可能会发生在请求了某个特定的URL之后

下面是WebSocket请求和响应的标识信息:

客户端的请求:

  • Connection属性中标识Upgrade,表示客户端希望连接升级
  • Upgrade属性中标识为Websocket,表示希望升级成 Websocket 协议
  • Sec-WebSocket-Key属性,表示随机字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 BASE-64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 Websocket 协议。
  • Sec-WebSocket-Version属性,表示支持的 Websocket 版本,RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用

服务器端响应:

  • Upgrade属性中标识为websocket
  • Connection告诉客户端即将升级的是 Websocket 协议
  • Sec-WebSocket-Accept这个则是经过服务器确认,并且加密过后的Sec-WebSocket-Key

Netty为WebSocket数据帧提供的支持

由 IETF 发布的WebSocket RFC,定义了6种帧,Netty为它们每种都提供了一个POJO实现

实战

首先,定义WebSocket服务端,其中创建了一个Netty提供ChannelGroup变量用来记录所有已经连接的客户端channel,而这个ChannelGroup就是用来完成群发和单聊功能的

//定义websocket服务端
public class WebSocketServer {

 private static EventLoopGroup bossGroup = new NioEventLoopGroup(1);
 private static EventLoopGroup workerGroup = new NioEventLoopGroup();
    private static ServerBootstrap bootstrap = new ServerBootstrap();

 private static final int PORT =8761;

 //创建 DefaultChannelGroup,用来保存所有已经连接的 WebSocket Channel,群发和一对一功能可以用上
 private final static ChannelGroup channelGroup =
            new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);

 public static void startServer(){
  try {
   bootstrap.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .childHandler(new WebSocketServerInitializer(channelGroup));
            Channel ch = bootstrap.bind(PORT).sync().channel();
            System.out.println("打开浏览器访问: http://127.0.0.1:" + PORT + '/');
            ch.closeFuture().sync();
  } catch (Exception e) {
   e.printStackTrace();
  }finally{
   bossGroup.shutdownGracefully();
         workerGroup.shutdownGracefully();
  }
 }
 public static void main(String[] args) {
  startServer();
 }
}

接下来,初始化Pipeline,向当前Pipeline中注册所有必需的ChannelHandler,主要包括:用于处理HTTP请求编解码的HttpServerCodec、自定义的处理HTTP请求的HttpRequestHandler、用于处理WebSocket帧数据以及升级握手的WebSocketServerProtocolHandler以及自定义的处理TextWebSocketFrame数据帧和握手完成事件的WebSocketServerHanlder

public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel>{

 /*websocket访问路径*/
    private static final String WEBSOCKET_PATH = "/ws";

    private ChannelGroup channelGroup;

 public WebSocketServerInitializer(ChannelGroup channelGroup){
  this.channelGroup=channelGroup;
 } 

 @Override
 protected void initChannel(SocketChannel ch) throws Exception {
  //用于HTTP请求的编解码
  ch.pipeline().addLast(new HttpServerCodec());
  //用于写入一个文件的内容
  ch.pipeline().addLast(new ChunkedWriteHandler());
  //用于http请求的聚合
  ch.pipeline().addLast(new HttpObjectAggregator(64*1024));
  //用于WebSocket应答数据压缩传输
  ch.pipeline().addLast(new WebSocketServerCompressionHandler());
  //处理http请求,对非websocket请求的处理
  ch.pipeline().addLast(new HttpRequestHandler(WEBSOCKET_PATH));
  //根据websocket规范,处理升级握手以及各种websocket数据帧
  ch.pipeline().addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, "", true));
  //对websocket的数据进行处理,主要处理TextWebSocketFrame数据帧和握手完成事件
  ch.pipeline().addLast(new WebSocketServerHanlder(channelGroup));
 }
}

HttpRequestHandler用来处理HTTP请求,首先会先确认当前的HTTP请求是否指向了WebSocket的URI,如果是那么HttpRequestHandler将调用FullHttpRequest对象上的retain方法,并通过调用fireChannelRead(msg)方法将它转发给下一个ChannelInboundHandler(之所以调用retain方法,是因为调用channelRead0方法完成之后,会进行资源释放)

接下来,读取磁盘上指定路径的index.html文件内容,将内容封装成ByteBuf对象,之后,构造一个FullHttpResponse响应对象,将ByteBuf添加进去,并设置请求头信息。最后,调用writeAndFlush方法冲刷所有写入的消息

public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest>{

 private static final File INDEX = new File("D:/学习/index.html");

 private String websocketUrl;

 public HttpRequestHandler(String websocketUrl)
 {
  this.websocketUrl = websocketUrl;
 }

 @Override
 protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
  if(websocketUrl.equalsIgnoreCase(msg.getUri())){
   //如果该HTTP请求指向了websocketUrl的URL,那么直接交给下一个ChannelInboundHandler进行处理
   ctx.fireChannelRead(msg.retain());
  }else{
   //生成index页面的具体内容,并送往浏览器
   ByteBuf content = loadIndexHtml(); 
   FullHttpResponse res = new DefaultFullHttpResponse(
                      HTTP_1_1, OK, content);

      res.headers().set(HttpHeaderNames.CONTENT_TYPE,
                      "text/html; charset=UTF-8");
      HttpUtil.setContentLength(res, content.readableBytes());
      sendHttpResponse(ctx, msg, res);
  }
 }

 public static ByteBuf loadIndexHtml(){
  FileInputStream fis = null;
  InputStreamReader isr = null;
  BufferedReader  raf = null;
  StringBuffer content = new StringBuffer();
  try {
     fis = new FileInputStream(INDEX);
     isr = new InputStreamReader(fis);
     raf = new BufferedReader(isr);
     String s = null;
     // 读取文件内容,并将其打印
     while((s = raf.readLine()) != null) {
     content.append(s);
     }
   } catch (Exception e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } finally {
   try {
    fis.close();
    isr.close();
    raf.close();
   } catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   }
  }
  return Unpooled.copiedBuffer(content.toString().getBytes());
 }
  /*发送应答*/
    private static void sendHttpResponse(ChannelHandlerContext ctx,
                                         FullHttpRequest req,
                                         FullHttpResponse res) {
        // 错误的请求进行处理 (code<>200).
        if (res.status().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(),
                    CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
            HttpUtil.setContentLength(res, res.content().readableBytes());
        }

        // 发送应答.
        ChannelFuture f = ctx.channel().writeAndFlush(res);
        //对于不是长连接或者错误的请求直接关闭连接
        if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }
}

前面的HttpRequestHandler处理器只是用来管理HTTP请求和响应的,而实际对传输的WebSocket数据帧的处理是交由WebSocketServerHanlder 进行(其中只对TextWebSocketFrame类型的数据帧进行处理)。

WebSocketServerHanlder 处理时通过重写userEventTriggered方法,并监听握手成功的事件,当新客户端的WebSocket握手成功之后,它将通过把通知消息写到ChannelGroup中的所有channel来通知所有已经连接的客户端,然后它将这个新的channel加入到该ChannelGroup中,并且还为每个channel随机生成了一个用户

之后,如果接收到了TextWebSocketFrame消息时,会先根据当前channel拿到用户,并解析发送的文本帧信息,确认是群聊还是单聊,最后,构造TextWebSocketFrame响应内容,通过writeAndFlush进行冲刷

/**
 * 对websocket的文本数据帧进行处理
 *
 */
public class WebSocketServerHanlder extends SimpleChannelInboundHandler<TextWebSocketFrame>{


 private ChannelGroup channelGroup;

 public WebSocketServerHanlder(ChannelGroup channelGroup){
  this.channelGroup=channelGroup;
 }

 @Override
 protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
  //获取当前channel用户名
  String userName=UserMap.getUser(ctx.channel().id().asLongText());
     //文本帧
  String content= msg.text();
  System.out.println("Client: "+ userName+" received [ "+content+" ]");
  String toName = null;
  //判断是单聊还是群发(单聊会通过  user@ msg 这种格式进行传输文本帧)
  if(content.contains("@")){
   String[] str= content.split("@");
   content=str[1];
   //获取单聊的用户
   toName = str[0];
  }
  if(null!=toName){
   Iterator<Channel> it=channelGroup.iterator();
   while(it.hasNext()){
    Channel channel=it.next();
    //找到指定的用户
    if(UserMap.getUser(channel.id().asLongText()).equals(toName)){
     //单聊
     channel.writeAndFlush(new TextWebSocketFrame(userName+"@"+content));
    }
   }
  }else{
   channelGroup.remove(ctx.channel());
   //群发实现
   channelGroup.writeAndFlush(new TextWebSocketFrame(userName+"@"+content));
   channelGroup.add(ctx.channel());
  }
 }
 @Override
 public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
  //检测事件,如果是握手成功事件,做点业务处理
  if(evt==WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE){
   String channelId = ctx.channel().id().asLongText();
   //随机为当前channel指定一个用户名
   UserMap.setUser(channelId);
   System.out.println("新的客户端连接:"+UserMap.getUser(channelId));
   //通知所有已经连接的 WebSocket 客户端新的客户端已经连接上了
   channelGroup.writeAndFlush(new TextWebSocketFrame(UserMap.getUser(channelId)+"加入群聊"));
   //将新的 WebSocket Channel 添加到 ChannelGroup 中
   channelGroup.add(ctx.channel());
  }else{
   super.userEventTriggered(ctx, evt);
  }
 }
}

index.html内容

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>基于WebSocket实现网页版群聊</title>
</head>
<body>
<script type="text/javascript">   
           var userName= null;        
           var socket;        
           var myDate = new Date();
           if (!window.WebSocket) {
                window.WebSocket = window.MozWebSocket;
            }
            if (window.WebSocket) {
                socket = new WebSocket("ws://127.0.0.1:8761/ws");
                socket.onmessage = function(event) { 
                   var info = document.getElementById("jp-container");
                   var dataObj=event.data;
                   if(dataObj.indexOf("@")!=-1){
                        var arr = dataObj.split('@');
                        var sendUser;
                        var acceptMsg;
                        for(var i=0;i<arr.length;i++){
                            if(i==0){
                                 sendUser = arr[i];
                            }else{
                                 acceptMsg =arr[i];
                            }
                        }
                      if(userName==sendUser){
                             return;
                      }        
                      var talk= document.createElement("div");
                      talk.setAttribute("class", "talk_recordboxme");
                      talk.innerHTML = sendUser+':';
                      var recordtext= document.createElement("div");
                      recordtext.setAttribute("class", "talk_recordtextbg");
                      talk.appendChild(recordtext);
                      var talk_recordtext=document.createElement("div");
                      talk_recordtext.setAttribute("class", " talk_recordtext");
                       var h3=document.createElement("h3");
                       h3.innerHTML =acceptMsg;
                       talk_recordtext.appendChild(h3);
                       var span=document.createElement("span");
                       span.innerHTML =myDate.toLocaleTimeString();
                       span.setAttribute("class", "talk_time");
                       talk_recordtext.appendChild(span);
                       talk.appendChild(talk_recordtext);
                   }else{
                       var talk= document.createElement("div");
                       talk.style.textAlign="center";
                       var font = document.createElement("font");
                       font.color='#212121';
                       font.innerHTML = dataObj+': '+myDate.toLocaleString( ); 
                       talk.appendChild(font);
                   }
                   info.appendChild(talk);
                };
                socket.onopen = function(event) {
                      console.log("Socket 已打开");
                };
                socket.onclose = function(event) {
                     console.log("Socket已关闭");
                  };
            } else {
                  alert("Your browser does not support Web Socket.");
            }
                function send(message) {
                    if (!window.WebSocket) { return; }
                       if (socket.readyState == WebSocket.OPEN) {
                         var info = document.getElementById("jp-container");

                   var talk= document.createElement("div");
                   talk.setAttribute("class", "talk_recordbox");

                    var user = document.createElement("div");
                    user.setAttribute("class", "user");
                    talk.appendChild(user);
                     var recordtext= document.createElement("div");

                   recordtext.setAttribute("class", "talk_recordtextbg");
                    talk.appendChild(recordtext);

                      var talk_recordtext=document.createElement("div");
                      talk_recordtext.setAttribute("class", " talk_recordtext");

                       var h3=document.createElement("h3");
                      h3.innerHTML =message;
                      talk_recordtext.appendChild(h3);
                     var span=document.createElement("span");
                      span.innerHTML =myDate.toLocaleTimeString();
                     span.setAttribute("class", "talk_time");
                      talk_recordtext.appendChild(span);
                     talk.appendChild(talk_recordtext);
                     info.appendChild(talk );
                          socket.send(message);
                     } else {
                           alert("The socket is not open.");
                      }
                }
</script>

<br>
<br>
<div class="talk">
 <div class="talk_title"><span>群聊</span></div>
 <div class="talk_record" style="background: #EEEEF4;">
  <div id="jp-container" class="jp-container">
  </div>

 </div>
  <form onsubmit="return false;">
       <div class="talk_word">
  &nbsp;
  <input class="add_face" id="facial" type="button" title="添加表情" value="" />
  <input class="messages emotion" autocomplete="off" name="message" value="在这里输入文字" onFocus="if(this.value=='在这里输入文字'){this.value='';}"  onblur="if(this.value==''){this.value='在这里输入文字';}"  />
  <input class="talk_send" type="button" title="发送" value="发送"  onclick="send(this.form.message.value)" />
       </div>
               </form> 
</div>

样式

body{
 font-family:verdana, Arial, Helvetica, "宋体", sans-serif;
 font-size: 12px;
}

body ,div ,dl ,dt ,dd ,ol ,li ,h1 ,h2 ,h3 ,h4 ,h5 ,h6 ,pre ,form ,fieldset ,input ,P ,blockquote ,th ,td ,img,
INS {
 margin: 0px;
 padding: 0px;
 border:0;
}
ol{
 list-style-type: none;
}
img,input{
 border:none;
}

a{
 color:#198DD0;
 text-decoration:none;
}
a:hover{
 color:#ba2636;
 text-decoration:underline;
}
a{blr:expression(this.onFocus=this.blur())}/*去掉a标签的虚线框,避免出现奇怪的选中区域*/
:focus{outline:0;}


.talk{
 height: 480px;
 width: 335px;
 margin:0 auto;
 border-left-width: 1px;
 border-left-style: solid;
 border-left-color: #444;
}
.talk_title{
 width: 100%;
 height:40px;
 line-height:40px;
 text-indent: 12px;
 font-size: 16px;
 font-weight: bold;
 color: #afafaf;
 background:#212121;
 border-bottom-width: 1px;
 border-bottom-style: solid;
 border-bottom-color: #434343;
 font-family: "微软雅黑";
}
.talk_title span{float:left}
.talk_title_c {
 width: 100%;
 height:30px;
 line-height:30px;
}
.talk_record{
 width: 100%;
 height:398px;
 overflow: hidden;
 border-bottom-width: 1px;
 border-bottom-style: solid;
 border-bottom-color: #434343;
 margin: 0px; 
}
.talk_word {
 line-height: 40px;
 height: 40px;
 width: 100%;
 background:#212121;
}
.messages {
 height: 24px;
 width: 240px;
 text-indent:5px;
 overflow: hidden;
 font-size: 12px;
 line-height: 24px;
 color: #666; 
 background-color: #ccc;
 border-radius: 3px;
 -moz-border-radius: 3px;
 -webkit-border-radius: 3px;
}
.messages:hover{background-color: #fff;}
.talk_send{
 width:50px;
 height:24px;
 line-height: 24px;
 font-size:12px;
 border:0px;
 margin-left: 2px;
 color: #fff;
 background-repeat: no-repeat;
 background-position: 0px 0px;
 background-color: transparent;
 font-family: "微软雅黑";
}
.talk_send:hover {
 background-position: 0px -24px;
}
.talk_record ul{ padding-left:5px;}
.talk_record li {
 line-height: 25px;
}
.talk_word .controlbtn a{
 margin: 12px;
}
.talk .talk_word .order {
 float:left;
 display: block;
 height: 14px;
 width: 16px;   
 background-repeat: no-repeat;
 background-position: 0px 0px;
}

.talk .talk_word .loop {
 float:left;
 display: block;
 height: 14px;
 width: 16px;
 background-repeat: no-repeat;
 background-position: -30px 0px;
}
.talk .talk_word .single {
 float:left;
 display: block;
 height: 14px;
 width: 16px;
 background-repeat: no-repeat;
 background-position: -60px 0px;
}
.talk .talk_word .order:hover,.talk .talk_word .active{
 background-position: 0px -20px;
 text-decoration: none;
}
.talk .talk_word .loop:hover{
 background-position: -30px -20px;
 text-decoration: none;
}
.talk .talk_word .single:hover{
 background-position: -60px -20px;
 text-decoration: none;
}


/*讨论区*/
.jp-container .talk_recordbox{
 min-height:80px;
 color: #afafaf;
 padding-top: 5px;
 padding-right: 10px;
 padding-left: 10px;
 padding-bottom: 0px;
}

.jp-container .talk_recordbox:first-child{border-top:none;}
.jp-container .talk_recordbox:last-child{border-bottom:none;}
.jp-container .talk_recordbox .talk_recordtextbg{
 float:left;
 width:10px;
 height:30px;
 display:block;
 background-repeat: no-repeat;
 background-position: left top;}
.jp-container .talk_recordbox .talk_recordtext{
 -moz-border-radius:5px;
 -webkit-border-radius:5px;
 border-radius:5px;
 background-color:#b8d45c;
 width:240px;
 height:auto;
 display:block;
 padding: 5px;
 float:left;
 color:#333333;
}
.jp-container .talk_recordbox h3{
 font-size:14px;
 padding:2px 0 5px 0;
 text-transform:uppercase;
 font-weight: 100;

}
.jp-container .talk_recordbox .user {
 float:left;
 display:inline;
 height: 45px;
 width: 45px;
 margin-top: 0px;
 margin-right: 5px;
 margin-bottom: 0px;
 margin-left: 0px;
 font-size: 12px;
 line-height: 20px;
 text-align: center;
}
/*自己发言样式*/
.jp-container .talk_recordboxme{
 display:block;
 min-height:80px;
 color: #afafaf; 
 padding-top: 5px;
 padding-right: 10px;
 padding-left: 10px;
 padding-bottom: 0px;
}
.jp-container .talk_recordboxme .talk_recordtextbg{
 float:right;
 width:10px;
 height:30px;
 display:block;
 background-repeat: no-repeat;
 background-position: left top;}

.jp-container .talk_recordboxme .talk_recordtext{
 -moz-border-radius:5px;
 -webkit-border-radius:5px;
 border-radius:5px;
 background-color:#fcfcfc;
 width:240px;
 height:auto;
 padding: 5px;
 color:#666;
 font-size:12px;
 float:right;

}
.jp-container .talk_recordboxme h3{
 font-size:14px;
 padding:2px 0 5px 0;
 text-transform:uppercase;
 font-weight: 100;
 color:#333333;

}
.jp-container .talk_recordboxme .user{
 float:right;
 height: 45px;
 width: 45px;
 margin-top: 0px;
 margin-right: 10px;
 margin-bottom: 0px;
 margin-left: 5px;
 font-size: 12px;
 line-height: 20px;
 text-align: center;
 display:inline;
}
.talk_time{
 color: #666;
 text-align: right;
 width: 240px;
 display: block;
}

测试

首先,启动三个窗口

群聊

单聊

总结

本文,基于Netty实战了一个WebSocket协议实现的网页版聊天室服务器,从代码上可以看出,基于Netty的WebSocket的实现还是非常简单、容易实现的。

但是WebSocket协议使用上还是存在局限的,比如需要浏览器的支持。但是毕竟WebSocket代表了Web技术的一种重要进展,可以扩宽我们的视野,在一些特定的工作场景中,可以帮助我们解决一些问题

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/1Gev5_WByERBHT1-1wrJvw

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237269次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8108次阅读
 目录