X723595506 4 dagar sedan
förälder
incheckning
4aa558af39
23 ändrade filer med 1262 tillägg och 37 borttagningar
  1. 51 37
      pom.xml
  2. 8 0
      src/main/java/com/example/course/ImApplication.java
  3. 16 0
      src/main/java/com/example/course/user/config/RedisConfig.java
  4. 6 0
      src/main/java/com/example/course/user/config/RedisKey.java
  5. 38 0
      src/main/java/com/example/course/user/config/RedissionConfig.java
  6. 88 0
      src/main/java/com/example/course/user/controller/WeChatController.java
  7. 20 0
      src/main/java/com/example/course/user/dao/WechatUserMapper.java
  8. 229 0
      src/main/java/com/example/course/user/domain/WechatUser.java
  9. 24 0
      src/main/java/com/example/course/user/dto/WXAuth.java
  10. 15 0
      src/main/java/com/example/course/user/service/WechatUserService.java
  11. 29 0
      src/main/java/com/example/course/user/service/impl/WechatUserServiceImpl.java
  12. 32 0
      src/main/java/com/example/course/utils/Constant.java
  13. 66 0
      src/main/java/com/example/course/utils/Result.java
  14. 56 0
      src/main/java/com/example/course/utils/StringTools.java
  15. 116 0
      src/main/java/com/example/course/utils/TokenUtils.java
  16. 124 0
      src/main/java/com/example/course/utils/WxService.java
  17. 25 0
      src/main/java/com/example/course/websocket/netty/HandlerHeartBeat.java
  18. 70 0
      src/main/java/com/example/course/websocket/netty/HandlerWebSocket.java
  19. 73 0
      src/main/java/com/example/course/websocket/netty/NettyWebSocket.java
  20. 6 0
      src/main/resources/application.yml
  21. 42 0
      src/main/resources/mapper/WechatUserMapper.xml
  22. 63 0
      src/test/java/com/example/course/SocketClient.java
  23. 65 0
      src/test/java/com/example/course/SocketService.java

+ 51 - 37
pom.xml

@@ -13,6 +13,13 @@
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
     </properties>
 
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.3.2.RELEASE</version>
+        <!--  使用合适的版本  -->
+    </parent>
+
     <dependencies>
 
         <dependency>
@@ -66,17 +73,11 @@
             <version>1.1.23</version>
         </dependency>
 
-        <dependency>
-            <groupId>com.baomidou</groupId>
-            <artifactId>mybatis-plus-boot-starter</artifactId>
-            <version>3.4.1</version>
-        </dependency>
-
-        <!--        <dependency>-->
-        <!--            <groupId>io.minio</groupId>-->
-        <!--            <artifactId>minio</artifactId>-->
-        <!--            <version>7.1.0</version>-->
-        <!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>io.minio</groupId>-->
+<!--            <artifactId>minio</artifactId>-->
+<!--            <version>7.1.0</version>-->
+<!--        </dependency>-->
 
         <dependency>
             <groupId>io.jsonwebtoken</groupId>
@@ -85,43 +86,56 @@
         </dependency>
 
         <!--    Redis-->
-        <!--        <dependency>-->
-        <!--            <groupId>org.springframework.boot</groupId>-->
-        <!--            <artifactId>spring-boot-starter-data-redis</artifactId>-->
-        <!--        </dependency>-->
+        <dependency>
+             <groupId>org.springframework.boot</groupId>
+             <artifactId>spring-boot-starter-data-redis</artifactId>
+            <version>2.3.2.RELEASE</version>
+        </dependency>
+
+<!--        redisson分布式锁-->
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson</artifactId>
+            <version>3.14.1</version>
+        </dependency>
 
         <!--        切面-->
-        <!--        <dependency>-->
-        <!--            <groupId>org.aspectj</groupId>-->
-        <!--            <artifactId>aspectjweaver</artifactId>-->
-        <!--        </dependency>-->
+<!--        <dependency>-->
+<!--             <groupId>org.aspectj</groupId>-->
+<!--             <artifactId>aspectjweaver</artifactId>-->
+<!--            <version>1.9.19</version>-->
+<!--        </dependency>-->
 
         <!--        okhttp-->
-<!--                <dependency>-->
-<!--                    <groupId>com.squareup.okhttp3</groupId>-->
-<!--                    <artifactId>okhttp</artifactId>-->
-<!--                </dependency>-->
+        <dependency>
+             <groupId>com.squareup.okhttp3</groupId>
+             <artifactId>okhttp</artifactId>
+            <version>4.9.3</version>
+        </dependency>
 
 <!--                fastjson-->
-<!--                <dependency>-->
-<!--                    <groupId>com.alibaba</groupId>-->
-<!--                    <artifactId>fastjson</artifactId>-->
-<!--                    <version>1.2.80</version>-->
-<!--                </dependency>-->
+        <dependency>
+             <groupId>com.alibaba</groupId>
+             <artifactId>fastjson</artifactId>
+             <version>2.0.41</version>
+        </dependency>
 
         <!--     commons-lang3   -->
-        <!--        <dependency>-->
-        <!--            <groupId>org.apache.commons</groupId>-->
-        <!--            <artifactId>commons-lang3</artifactId>-->
-        <!--        </dependency>-->
+        <dependency>
+             <groupId>org.apache.commons</groupId>
+             <artifactId>commons-lang3</artifactId>
+             <version>3.17.0</version>
+        </dependency>
 
-        <!--        netty-all-->
-<!--                <dependency>-->
-<!--                    <groupId>io.netty</groupId>-->
-<!--                    <artifactId>netty-all</artifactId>-->
-<!--                </dependency>-->
+<!--                netty-all-->
+        <dependency>
+             <groupId>io.netty</groupId>
+             <artifactId>netty-all</artifactId>
+             <version>4.1.84.Final</version>
+        </dependency>
 
     </dependencies>
+
     <build>
         <plugins>
             <plugin>

+ 8 - 0
src/main/java/com/example/course/ImApplication.java

@@ -1,12 +1,20 @@
 package com.example.course;
 
+import com.example.course.websocket.netty.NettyWebSocket;
+import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 
+import javax.annotation.Resource;
+
 @SpringBootApplication
 public class ImApplication {
 
+//    @Resource
+//    private static NettyWebSocket nettyWebSocket;
+
     public static void main(String[] args) {
+//        nettyWebSocket.startNetty();
         SpringApplication.run(ImApplication.class, args);
     }
 

+ 16 - 0
src/main/java/com/example/course/user/config/RedisConfig.java

@@ -0,0 +1,16 @@
+package com.example.course.user.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+@Configuration
+public class RedisConfig {
+    @Bean
+    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
+        StringRedisTemplate template = new StringRedisTemplate();
+        template.setConnectionFactory(connectionFactory);
+        return template;
+    }
+}

+ 6 - 0
src/main/java/com/example/course/user/config/RedisKey.java

@@ -0,0 +1,6 @@
+package com.example.course.user.config;
+
+public class RedisKey {
+    public static final String WX_SESSION_ID = "wxSessionId:";
+    public static final String USER_TOKEN = "userToken:";
+}

+ 38 - 0
src/main/java/com/example/course/user/config/RedissionConfig.java

@@ -0,0 +1,38 @@
+package com.example.course.user.config;
+
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class RedissionConfig {
+    @Value("${spring.redis.host}")
+    private String host;
+    @Value("${spring.redis.port}")
+    private Integer port;
+    @Value("${spring.redis.database}")
+    private Integer database;
+    @Value("${spring.redis.password}")
+    private String password;
+
+    @Bean
+    public RedissonClient getRedisson(){
+        Config config = new Config();
+        config.useSingleServer().setAddress("redis://" + host + ":" + port)
+                .setDatabase(database);
+        if (StringUtils.isNotEmpty(password)){
+            config.useSingleServer().setAddress("redis://" + host + ":" + port).setDatabase(database)
+                    .setPassword(password);
+        }else{
+            config.useSingleServer().setAddress("redis://" + host + ":" + port).setDatabase(database);
+        }
+        //设置全局默认看门狗机制续期时间,如果在使用时不设置,则使用全局的,如果全局不设置,则使用默认的30000,单位毫秒
+        config.setLockWatchdogTimeout(2000);
+        return Redisson.create(config);
+    }
+
+}

+ 88 - 0
src/main/java/com/example/course/user/controller/WeChatController.java

@@ -0,0 +1,88 @@
+package com.example.course.user.controller;
+
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson2.JSONObject;
+import com.example.course.user.domain.WechatUser;
+import com.example.course.user.dto.WXAuth;
+import com.example.course.user.service.WechatUserService;
+import com.example.course.utils.Result;
+//import io.swagger.annotations.Api;
+//import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+
+/**
+ * 微信相关接口控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/wechat")
+public class WeChatController {
+
+    @Autowired
+    private WechatUserService wechatUserService;
+
+    @RequestMapping("/login")
+    public void wx(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        //第一步:引导用户进入授权页面同意授权,获取code
+        StringBuilder builder = new StringBuilder("https://open.weixin.qq.com/connect/oauth2/authorize?appid=");
+        builder.append("wxf463750c0be078db");
+        builder.append("&redirect_uri=");
+        //https://5ccy.cn/guest/callBack 是自己后台的回调地址
+        builder.append(URLEncoder.encode("https://5ccy.cn/guest/callBack"));
+        builder.append("&response_type=code");
+        builder.append("&scope=snsapi_userinfo");
+        builder.append("&state=STATE#wechat_redirect");
+        //授权页面地址
+        //将StringBuilder转换成String
+        String url = builder.toString();
+        //重定向到授权页面
+        response.sendRedirect(url);
+    }
+
+    /**
+     * 第二步页面授权后根据上面回调地址请求到此接口上
+     */
+    @RequestMapping(value = "/callBack")
+    public String wxcallback(@RequestParam("code") String code) throws IOException {
+        System.out.println("code:" + code);
+        //获取code后,请求以下链接获取access_token
+        StringBuilder builder = new StringBuilder("https://api.weixin.qq.com/sns/oauth2/access_token?appid=");
+        builder.append("wxf463750c0be078db");
+        builder.append("&secret=");
+        builder.append("beba38298a2f51c6299703dc31744e12");
+        builder.append("&code=");
+        builder.append(code);
+        builder.append("&grant_type=authorization_code");
+
+
+        String url = builder.toString();
+        //通过网络请求方法来请求上面这个接口从返回的JSON数据中取出access_token和openid
+        String resAccessToken = HttpUtil.get(url);
+        JSONObject parse = JSONObject.parse(resAccessToken);
+        String access_token = parse.getString("access_token");
+        String refresh_token = parse.getString("refresh_token");
+        String openid = parse.getString("openid");
+        String scope = parse.getString("scope");
+        Integer expiresIn = parse.getInteger("expires_in");
+
+        //拉取用户信息(需scope为 snsapi_userinfo)
+        StringBuilder builder1 = new StringBuilder("https://api.weixin.qq.com/sns/userinfo?access_token=");
+        //access_token是上面返回的值
+        builder1.append(access_token);
+        builder1.append("&openid=");
+        builder1.append(openid);
+        builder1.append("&lang=zh_CN");
+        //通过网络请求方法来请求上面这个接口
+        String resInfo = HttpUtil.get(builder1.toString());
+
+        return resInfo ;
+    }
+
+}

+ 20 - 0
src/main/java/com/example/course/user/dao/WechatUserMapper.java

@@ -0,0 +1,20 @@
+package com.example.course.user.dao;
+
+import com.example.course.user.domain.WechatUser;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @author 徐乐
+* @description 针对表【wechat_user(微信用户表)】的数据库操作Mapper
+* @createDate 2025-05-19 16:54:07
+* @Entity com.example.course.domain.WechatUser
+*/
+@Mapper
+public interface WechatUserMapper extends BaseMapper<WechatUser> {
+
+}
+
+
+
+

+ 229 - 0
src/main/java/com/example/course/user/domain/WechatUser.java

@@ -0,0 +1,229 @@
+package com.example.course.user.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import java.io.Serializable;
+import java.util.Date;
+import lombok.Data;
+
+/**
+ * 微信用户表
+ * @TableName wechat_user
+ */
+@TableName(value ="wechat_user")
+@Data
+public class WechatUser implements Serializable {
+    /**
+     * 主键ID
+     */
+    @TableId(type = IdType.AUTO)
+    private Long wechatId;
+
+    /**
+     * 微信开放ID(应用唯一)
+     */
+    private String openid;
+
+    /**
+     * 微信联合ID(跨应用唯一)
+     */
+    private String unionid;
+
+    /**
+     * 微信应用ID
+     */
+    private String appId;
+
+    /**
+     * 微信昵称
+     */
+    private String nickname;
+
+    /**
+     * 微信头像
+     */
+    private String avatarUrl;
+
+    /**
+     * 性别(0:未知 1:男 2:女)
+     */
+    private Integer gender;
+
+    /**
+     * 国家
+     */
+    private String country;
+
+    /**
+     * 省份
+     */
+    private String province;
+
+    /**
+     * 城市
+     */
+    private String city;
+
+    /**
+     * 语言
+     */
+    private String language;
+
+    /**
+     * 小程序会话密钥
+     */
+    private String sessionKey;
+
+    /**
+     * 访问令牌
+     */
+    private String accessToken;
+
+    /**
+     * 刷新令牌
+     */
+    private String refreshToken;
+
+    /**
+     * 令牌过期时间(秒)
+     */
+    private Integer expiresIn;
+
+    /**
+     * 绑定手机号
+     */
+    private String mobile;
+
+    /**
+     * 绑定邮箱
+     */
+    private String email;
+
+    /**
+     * 状态(0:禁用 1:正常)
+     */
+    private Integer status;
+
+    /**
+     * 最后登录时间
+     */
+    private Date lastLoginTime;
+
+    /**
+     * 最后登录IP
+     */
+    private String lastLoginIp;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    private Date updateTime;
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    public boolean equals(Object that) {
+        if (this == that) {
+            return true;
+        }
+        if (that == null) {
+            return false;
+        }
+        if (getClass() != that.getClass()) {
+            return false;
+        }
+        WechatUser other = (WechatUser) that;
+        return (this.getWechatId() == null ? other.getWechatId() == null : this.getWechatId().equals(other.getWechatId()))
+            && (this.getOpenid() == null ? other.getOpenid() == null : this.getOpenid().equals(other.getOpenid()))
+            && (this.getUnionid() == null ? other.getUnionid() == null : this.getUnionid().equals(other.getUnionid()))
+            && (this.getAppId() == null ? other.getAppId() == null : this.getAppId().equals(other.getAppId()))
+            && (this.getNickname() == null ? other.getNickname() == null : this.getNickname().equals(other.getNickname()))
+            && (this.getAvatarUrl() == null ? other.getAvatarUrl() == null : this.getAvatarUrl().equals(other.getAvatarUrl()))
+            && (this.getGender() == null ? other.getGender() == null : this.getGender().equals(other.getGender()))
+            && (this.getCountry() == null ? other.getCountry() == null : this.getCountry().equals(other.getCountry()))
+            && (this.getProvince() == null ? other.getProvince() == null : this.getProvince().equals(other.getProvince()))
+            && (this.getCity() == null ? other.getCity() == null : this.getCity().equals(other.getCity()))
+            && (this.getLanguage() == null ? other.getLanguage() == null : this.getLanguage().equals(other.getLanguage()))
+            && (this.getSessionKey() == null ? other.getSessionKey() == null : this.getSessionKey().equals(other.getSessionKey()))
+            && (this.getAccessToken() == null ? other.getAccessToken() == null : this.getAccessToken().equals(other.getAccessToken()))
+            && (this.getRefreshToken() == null ? other.getRefreshToken() == null : this.getRefreshToken().equals(other.getRefreshToken()))
+            && (this.getExpiresIn() == null ? other.getExpiresIn() == null : this.getExpiresIn().equals(other.getExpiresIn()))
+            && (this.getMobile() == null ? other.getMobile() == null : this.getMobile().equals(other.getMobile()))
+            && (this.getEmail() == null ? other.getEmail() == null : this.getEmail().equals(other.getEmail()))
+            && (this.getStatus() == null ? other.getStatus() == null : this.getStatus().equals(other.getStatus()))
+            && (this.getLastLoginTime() == null ? other.getLastLoginTime() == null : this.getLastLoginTime().equals(other.getLastLoginTime()))
+            && (this.getLastLoginIp() == null ? other.getLastLoginIp() == null : this.getLastLoginIp().equals(other.getLastLoginIp()))
+            && (this.getCreateTime() == null ? other.getCreateTime() == null : this.getCreateTime().equals(other.getCreateTime()))
+            && (this.getUpdateTime() == null ? other.getUpdateTime() == null : this.getUpdateTime().equals(other.getUpdateTime()));
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((getWechatId() == null) ? 0 : getWechatId().hashCode());
+        result = prime * result + ((getOpenid() == null) ? 0 : getOpenid().hashCode());
+        result = prime * result + ((getUnionid() == null) ? 0 : getUnionid().hashCode());
+        result = prime * result + ((getAppId() == null) ? 0 : getAppId().hashCode());
+        result = prime * result + ((getNickname() == null) ? 0 : getNickname().hashCode());
+        result = prime * result + ((getAvatarUrl() == null) ? 0 : getAvatarUrl().hashCode());
+        result = prime * result + ((getGender() == null) ? 0 : getGender().hashCode());
+        result = prime * result + ((getCountry() == null) ? 0 : getCountry().hashCode());
+        result = prime * result + ((getProvince() == null) ? 0 : getProvince().hashCode());
+        result = prime * result + ((getCity() == null) ? 0 : getCity().hashCode());
+        result = prime * result + ((getLanguage() == null) ? 0 : getLanguage().hashCode());
+        result = prime * result + ((getSessionKey() == null) ? 0 : getSessionKey().hashCode());
+        result = prime * result + ((getAccessToken() == null) ? 0 : getAccessToken().hashCode());
+        result = prime * result + ((getRefreshToken() == null) ? 0 : getRefreshToken().hashCode());
+        result = prime * result + ((getExpiresIn() == null) ? 0 : getExpiresIn().hashCode());
+        result = prime * result + ((getMobile() == null) ? 0 : getMobile().hashCode());
+        result = prime * result + ((getEmail() == null) ? 0 : getEmail().hashCode());
+        result = prime * result + ((getStatus() == null) ? 0 : getStatus().hashCode());
+        result = prime * result + ((getLastLoginTime() == null) ? 0 : getLastLoginTime().hashCode());
+        result = prime * result + ((getLastLoginIp() == null) ? 0 : getLastLoginIp().hashCode());
+        result = prime * result + ((getCreateTime() == null) ? 0 : getCreateTime().hashCode());
+        result = prime * result + ((getUpdateTime() == null) ? 0 : getUpdateTime().hashCode());
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName());
+        sb.append(" [");
+        sb.append("Hash = ").append(hashCode());
+        sb.append(", wechatId=").append(wechatId);
+        sb.append(", openid=").append(openid);
+        sb.append(", unionid=").append(unionid);
+        sb.append(", appId=").append(appId);
+        sb.append(", nickname=").append(nickname);
+        sb.append(", avatarUrl=").append(avatarUrl);
+        sb.append(", gender=").append(gender);
+        sb.append(", country=").append(country);
+        sb.append(", province=").append(province);
+        sb.append(", city=").append(city);
+        sb.append(", language=").append(language);
+        sb.append(", sessionKey=").append(sessionKey);
+        sb.append(", accessToken=").append(accessToken);
+        sb.append(", refreshToken=").append(refreshToken);
+        sb.append(", expiresIn=").append(expiresIn);
+        sb.append(", mobile=").append(mobile);
+        sb.append(", email=").append(email);
+        sb.append(", status=").append(status);
+        sb.append(", lastLoginTime=").append(lastLoginTime);
+        sb.append(", lastLoginIp=").append(lastLoginIp);
+        sb.append(", createTime=").append(createTime);
+        sb.append(", updateTime=").append(updateTime);
+        sb.append(", serialVersionUID=").append(serialVersionUID);
+        sb.append("]");
+        return sb.toString();
+    }
+}

+ 24 - 0
src/main/java/com/example/course/user/dto/WXAuth.java

@@ -0,0 +1,24 @@
+package com.example.course.user.dto;
+
+//import io.swagger.annotations.ApiModel;
+//import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 微信授权信息DTO
+ */
+@Data
+public class WXAuth {
+
+//    @ApiModelProperty(value = "微信授权码", required = true)
+
+    private String code;
+
+//    @ApiModelProperty(value = "加密数据")
+    private String encryptedData;
+
+//    @ApiModelProperty(value = "初始向量")
+    private String iv;
+}

+ 15 - 0
src/main/java/com/example/course/user/service/WechatUserService.java

@@ -0,0 +1,15 @@
+package com.example.course.user.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.example.course.user.domain.WechatUser;
+import com.example.course.user.dto.WXAuth;
+import com.example.course.utils.Result;
+
+/**
+* @author 徐乐
+* @description 针对表【wechat_users(微信用户信息表)】的数据库操作Service
+* @createDate 2025-05-04 13:48:32
+*/
+public interface WechatUserService extends IService<WechatUser> {
+
+}

+ 29 - 0
src/main/java/com/example/course/user/service/impl/WechatUserServiceImpl.java

@@ -0,0 +1,29 @@
+package com.example.course.user.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.course.user.dao.WechatUserMapper;
+import com.example.course.user.domain.WechatUser;
+import com.example.course.user.dto.WXAuth;
+import com.example.course.user.service.WechatUserService;
+import com.example.course.utils.Result;
+import com.example.course.utils.TokenUtils;
+import com.example.course.utils.WxService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 微信用户服务实现类
+ */
+@Service
+public class WechatUserServiceImpl extends ServiceImpl<WechatUserMapper, WechatUser> implements WechatUserService {
+
+}

+ 32 - 0
src/main/java/com/example/course/utils/Constant.java

@@ -0,0 +1,32 @@
+package com.example.course.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @ClassName: Constant
+ * @Author:
+ * @Date: 2024年2月26日 14:05
+ */
+public class Constant {
+
+
+    public static String TOKEN_NAME = "Authorization";
+
+    /**
+     * 请求响应码常量
+     */
+    public static final Integer RESPONSE_CODE_SUCCESS = 200;
+    public static final Integer RESPONSE_CODE_ERROR = 400;
+    public static final Integer RESPONSE_CODE_NO_LOGIN = 402;
+    public static final Integer RESPONSE_CODE_FORBIDDEN = 403;
+
+    public static final Map<Integer, String> RESPONSE_CODE_MAP = new HashMap<>();
+    static {
+        RESPONSE_CODE_MAP.put(RESPONSE_CODE_SUCCESS, "请求成功");
+        RESPONSE_CODE_MAP.put(RESPONSE_CODE_ERROR, "请求失败");
+        RESPONSE_CODE_MAP.put(RESPONSE_CODE_NO_LOGIN, "没有登录");
+        RESPONSE_CODE_MAP.put(RESPONSE_CODE_FORBIDDEN, "权限不足");
+    }
+
+}

+ 66 - 0
src/main/java/com/example/course/utils/Result.java

@@ -0,0 +1,66 @@
+package com.example.course.utils;
+
+import lombok.Data;
+
+/**
+ * @ClassName: Result 返回结果
+ *  code 响应码 代表请求成功/失败
+ *  message 响应信息
+ *  data 响应数据
+ * @Author:
+ * @Date: 2024年2月26日 13:58
+ */
+@Data
+public class Result {
+    private Integer code;//状态码
+    private String message;//提示信息
+    private Object data;//数据
+
+    public Result(Integer code, String message, Object data) {
+        this.code = code;
+        this.message = message;
+        this.data = data;
+    }
+
+    public Result() {
+    }
+
+    //成功响应
+    public static Result OK() {
+        return new Result(Constant.RESPONSE_CODE_SUCCESS, "操作成功", null);
+    }
+
+    public static Result OK(String message,Object data) {
+        return new Result(Constant.RESPONSE_CODE_SUCCESS, message, data);
+    }
+
+    //失败响应
+    public static Result ERROR() {
+        return new Result(Constant.RESPONSE_CODE_ERROR, "操作失败", null);
+    }
+
+    public static Result ERROR(Integer code,String message) {
+        return new Result(code, message, null);
+    }
+
+    public static Result ERROR(Object data) {
+        return new Result(Constant.RESPONSE_CODE_ERROR, "操作失败", data);
+    }
+
+    public static Result ERROR(String message) {
+        return new Result(Constant.RESPONSE_CODE_ERROR, message, null);
+    }
+
+    //未登录响应
+    public static Result NO_LOGIN(){
+        return new Result(Constant.RESPONSE_CODE_NO_LOGIN, "未登录", null);
+    }
+    public static Result NO_LOGIN(String message){
+        return new Result(Constant.RESPONSE_CODE_NO_LOGIN, message, null);
+    }
+
+    //权限不足响应
+    public static Result FORBIDDEN(){
+        return new Result(Constant.RESPONSE_CODE_FORBIDDEN, "权限不足,禁止访问", null);
+    }
+}

+ 56 - 0
src/main/java/com/example/course/utils/StringTools.java

@@ -0,0 +1,56 @@
+package com.example.course.utils;
+
+public class StringTools {
+
+    /**
+     * 判断字符串是否为空或仅包含空白字符
+     *
+     * @param str 要检查的字符串
+     * @return 如果字符串为空或仅包含空白字符,则返回 true,否则返回 false
+     */
+    public static boolean isEmpty(final CharSequence str) {
+        return str == null || str.length() == 0;
+    }
+
+    /**
+     * 判断字符串是否为空或仅包含空白字符
+     *
+     * @param str 要检查的字符串
+     * @return 如果字符串为空或仅包含空白字符,则返回 true,否则返回 false
+     */
+    public static boolean isBlank(final CharSequence str) {
+        return str == null || str.length() == 0 || isWhitespace(str);
+    }
+
+    /**
+     * 判断字符序列是否为空白字符
+     *
+     * @param cs 要检查的字符序列
+     * @return 如果字符序列为空白字符,则返回 true,否则返回 false
+     */
+    public static boolean isWhitespace(final CharSequence cs) {
+        if (cs == null) {
+            return false;
+        }
+        for (int i = 0; i < cs.length(); i++) {
+            if (!Character.isWhitespace(cs.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 查找指定字符在字符串中首次出现的位置
+     *
+     * @param str    要搜索的字符串
+     * @param ch     要查找的字符
+     * @return 如果找到指定字符,则返回其索引,否则返回 -1
+     */
+    public static int indexOf(final String str, final char ch) {
+        if (str == null) {
+            return -1;
+        }
+        return str.indexOf(ch);
+    }
+}

+ 116 - 0
src/main/java/com/example/course/utils/TokenUtils.java

@@ -0,0 +1,116 @@
+package com.example.course.utils;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Token工具类
+ */
+@Slf4j
+public class TokenUtils {
+
+    // 密钥
+    private static final String SECRET_KEY = "im_wechat_secret_key_2023";
+
+    // token有效期(7天)
+    private static final long EXPIRATION_TIME = 7 * 24 * 60 * 60 * 1000;
+
+    // token前缀
+    private static final String TOKEN_PREFIX = "Bearer ";
+
+    // header中的token键名
+    private static final String HEADER_STRING = "Authorization";
+
+    /**
+     * 生成token
+     * @param userId 用户ID
+     * @return token字符串
+     */
+    public static String generateToken(Long userId) {
+        Map<String, Object> claims = new HashMap<>();
+        claims.put("userId", userId);
+
+        return Jwts.builder()
+                .setClaims(claims)
+                .setSubject(userId.toString())
+                .setIssuedAt(new Date())
+                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
+                .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
+                .compact();
+    }
+
+    /**
+     * 从token中获取用户ID
+     * @param token token字符串
+     * @return 用户ID
+     */
+    public static Long getUserIdFromToken(String token) {
+        try {
+            Claims claims = Jwts.parser()
+                    .setSigningKey(SECRET_KEY)
+                    .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
+                    .getBody();
+
+            return Long.parseLong(claims.getSubject());
+        } catch (Exception e) {
+            log.error("从token中获取用户ID失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 验证token是否有效
+     * @param token token字符串
+     * @return 是否有效
+     */
+    public static boolean validateToken(String token) {
+        try {
+            Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token.replace(TOKEN_PREFIX, ""));
+            return true;
+        } catch (Exception e) {
+            log.error("验证token失败", e);
+            return false;
+        }
+    }
+
+    /**
+     * 从请求上下文中获取token
+     * @return token字符串
+     */
+    public static String getTokenFromContext() {
+        try {
+            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+            if (attributes != null) {
+                HttpServletRequest request = attributes.getRequest();
+                String token = request.getHeader(HEADER_STRING);
+                if (token != null && token.startsWith(TOKEN_PREFIX)) {
+                    return token.replace(TOKEN_PREFIX, "");
+                }
+            }
+        } catch (Exception e) {
+            log.error("从请求上下文中获取token失败", e);
+        }
+        return null;
+    }
+
+    /**
+     * 从请求上下文中获取用户ID
+     * @return 用户ID
+     */
+    public static Long getUserIdFromContext() {
+        String token = getTokenFromContext();
+        if (token != null) {
+            return getUserIdFromToken(token);
+        }
+        return null;
+    }
+}

+ 124 - 0
src/main/java/com/example/course/utils/WxService.java

@@ -0,0 +1,124 @@
+package com.example.course.utils;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * 微信服务工具类
+ */
+@Slf4j
+@Component
+public class WxService {
+
+    @Value("${wx.appId}")
+    private String appId;
+
+    @Value("${wx.appSecret}")
+    private String appSecret;
+
+    private final String WX_AUTH_URL = "https://api.weixin.qq.com/sns/jscode2session";
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    /**
+     * 通过code获取微信用户信息
+     * @param code 微信授权码
+     * @return 微信用户信息
+     */
+    public JSONObject getWxUserInfo(String code) {
+        try {
+            // 构建请求URL
+            String url = String.format("%s?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
+                    WX_AUTH_URL, appId, appSecret, code);
+
+            // 发送GET请求到微信服务器
+            ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
+            String body = response.getBody();
+
+            if (body != null && !body.isEmpty()) {
+                JSONObject jsonObject = JSON.parseObject(body);
+
+                // 检查返回结果是否包含错误信息
+                if (jsonObject.containsKey("errcode") && jsonObject.getInteger("errcode") != 0) {
+                    log.error("获取微信用户信息失败: {}", jsonObject.getString("errmsg"));
+                    return null;
+                }
+
+                return jsonObject;
+            }
+        } catch (Exception e) {
+            log.error("调用微信接口失败", e);
+        }
+        return null;
+    }
+
+    /**
+     * 验证微信签名
+     * @param signature 微信签名
+     * @param timestamp 时间戳
+     * @param nonce 随机字符串
+     * @param data 数据
+     * @return 验证结果
+     */
+    public boolean checkSignature(String signature, String timestamp, String nonce, String data) {
+        try {
+            // TODO: 实现微信签名验证逻辑
+            // 1. 将timestamp、nonce、data进行字典序排序
+            // 2. 将三个参数字符串拼接成一个字符串
+            // 3. 进行sha1加密
+            // 4. 将加密后的字符串与signature对比
+            return true;
+        } catch (Exception e) {
+            log.error("验证微信签名失败", e);
+            return false;
+        }
+    }
+
+    /**
+     * 解密微信数据
+     * @param encryptedData 加密数据
+     * @param sessionKey 会话密钥
+     * @param iv 初始向量
+     * @return 解密后的数据
+     */
+    public JSONObject decryptWxData(String encryptedData, String sessionKey, String iv) {
+        try {
+            // TODO: 实现微信数据解密逻辑
+            // 1. 使用Base64解码encryptedData和sessionKey
+            // 2. 使用AES-128-CBC解密,使用sessionKey作为密钥,iv作为初始向量
+            // 3. 解析解密后的JSON数据
+            return null;
+        } catch (Exception e) {
+            log.error("解密微信数据失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 获取微信接口调用凭证
+     * @return 接口调用凭证
+     */
+    public String getAccessToken() {
+        try {
+            String url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
+                    appId, appSecret);
+
+            ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
+            String body = response.getBody();
+
+            if (body != null && !body.isEmpty()) {
+                JSONObject jsonObject = JSON.parseObject(body);
+                if (!jsonObject.containsKey("errcode")) {
+                    return jsonObject.getString("access_token");
+                }
+            }
+        } catch (Exception e) {
+            log.error("获取微信接口调用凭证失败", e);
+        }
+        return null;
+    }
+}

+ 25 - 0
src/main/java/com/example/course/websocket/netty/HandlerHeartBeat.java

@@ -0,0 +1,25 @@
+package com.example.course.websocket.netty;
+
+import io.netty.channel.ChannelDuplexHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.timeout.IdleState;
+import io.netty.handler.timeout.IdleStateEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class HandlerHeartBeat extends ChannelDuplexHandler {
+    private static final Logger logger = LoggerFactory.getLogger(HandlerHeartBeat.class);
+    @Override
+    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+        if (evt instanceof IdleStateEvent){
+            IdleStateEvent e = (IdleStateEvent) evt;
+            if (e.state() == IdleState.READER_IDLE){
+                logger.info("心跳超时");
+                ctx.close();
+            }else if(e.state() == IdleState.WRITER_IDLE){
+                ctx.writeAndFlush("heart");
+            }
+
+        }
+    }
+}

+ 70 - 0
src/main/java/com/example/course/websocket/netty/HandlerWebSocket.java

@@ -0,0 +1,70 @@
+package com.example.course.websocket.netty;
+
+import com.example.course.utils.StringTools;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class HandlerWebSocket extends SimpleChannelInboundHandler<TextWebSocketFrame> {
+    private static final Logger logger = LoggerFactory.getLogger(HandlerHeartBeat.class);
+
+
+    /**
+     * 通道就绪后 调用,一般用来做初始化
+     * @param ctx
+     * @throws Exception
+     */
+
+    @Override
+    public void channelActive(ChannelHandlerContext ctx) throws Exception {
+        logger.info("有新的连接加入....");
+    }
+
+    @Override
+    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+        logger.info("有连接断开....");
+    }
+
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
+        Channel channel = ctx.channel();
+        logger.info("收到消息:{}",msg.text());
+    }
+
+    @Override
+    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+        if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete){
+            WebSocketServerProtocolHandler.HandshakeComplete complete = (WebSocketServerProtocolHandler.HandshakeComplete) evt;
+            String url = complete.requestUri();
+            String token = getToken(url);
+            if (token == null){
+                ctx.channel().close();
+                return;
+            }
+            logger.info("url:{}",url);
+        }
+        super.userEventTriggered(ctx, evt);
+    }
+
+    private String getToken(String url){
+        // 判断
+        if (StringTools.isEmpty(url) || url.indexOf("?")==-1){
+            return null;
+        }
+        String[] queryParams = url.split("\\?");
+        if (queryParams.length!=2){
+            return null;
+        }
+        String[] params = queryParams[1].split("=");
+        if (params.length!=2){
+            return null;
+        }
+        return params[1];
+    }
+}

+ 73 - 0
src/main/java/com/example/course/websocket/netty/NettyWebSocket.java

@@ -0,0 +1,73 @@
+package com.example.course.websocket.netty;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.*;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
+import io.netty.handler.timeout.IdleStateHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PreDestroy;
+import javax.annotation.Resource;
+import java.util.concurrent.TimeUnit;
+@Component
+public class NettyWebSocket {
+    private static final Logger logger = LoggerFactory.getLogger(NettyWebSocket.class);
+    private static EventLoopGroup bossGroup = new NioEventLoopGroup(1);
+    private static EventLoopGroup workerGroup = new NioEventLoopGroup();
+
+    @Resource
+    private HandlerWebSocket handlerWebSocket;
+
+    @PreDestroy
+    public void close(){
+        bossGroup.shutdownGracefully();
+        workerGroup.shutdownGracefully();
+    }
+    @Async
+    public void startNetty(){
+        try{
+            ServerBootstrap serverBootstrap = new ServerBootstrap();
+            serverBootstrap.group(bossGroup,workerGroup);
+            serverBootstrap.channel(NioServerSocketChannel.class)
+                    .handler(new LoggingHandler(LogLevel.DEBUG)).childHandler(new ChannelInitializer() {
+                        @Override
+                        protected void initChannel(Channel channel) throws Exception {
+                            // 添加管道
+                            ChannelPipeline pipeline = channel.pipeline();
+                            // 设置几个重要的处理器
+                            // 添加http编解码器 支持http协议
+                            pipeline.addLast(new HttpServerCodec());
+                            // 聚合解码 httpRequest/httpContent/lastHttpContent到fullHttpRequest
+                            // 保证接收的http请求的完整性
+                            pipeline.addLast(new HttpObjectAggregator(64*1024));
+                            // 添加心跳检测 long readerIdleTime, long writerIdleTime, long allIdleTime,TimeUnit unit
+                            // readerIdleTime:读超时时间,即测试端一定时间内未接受到被测试端消息
+                            // writerIdleTime:写超时时间,即测试端一定时间内向被测试端发送消息
+                            // allIdleTime: 所有类型的超时时间
+                            pipeline.addLast(new IdleStateHandler(6,0,0, TimeUnit.SECONDS));
+                            pipeline.addLast(new HandlerHeartBeat());
+                            //将http协议升级为ws协议,对websocket支持
+                            pipeline.addLast(new WebSocketServerProtocolHandler("/ws",null,true,64*1024,true,true,10000L));
+                            pipeline.addLast(handlerWebSocket);
+                        }
+                    });
+            ChannelFuture channelFuture = serverBootstrap.bind(5051).sync();
+            logger.info("netty启动成功");
+            channelFuture.channel().closeFuture().sync();
+        }catch (Exception e){
+            logger.error("启动netty失败",e);
+        }finally {
+            bossGroup.shutdownGracefully();
+            workerGroup.shutdownGracefully();
+        }
+    }
+}

+ 6 - 0
src/main/resources/application.yml

@@ -7,6 +7,11 @@ spring:
     url: jdbc:mysql://47.111.97.7:3306/IM?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
     username: root
     password: root
+  redis:
+    host: 127.0.0.1
+    port: 6379
+    database: 0
+    password:
 mybatis-plus:
   type-aliases-package: com.example.course
   configuration:
@@ -18,3 +23,4 @@ logging:
     com:
       example:
         course: debug
+

+ 42 - 0
src/main/resources/mapper/WechatUserMapper.xml

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.example.course.user.dao.WechatUserMapper">
+
+    <resultMap id="BaseResultMap" type="com.example.course.user.domain.WechatUser">
+            <id property="wechatId" column="wechat_id" jdbcType="BIGINT"/>
+            <result property="openid" column="openid" jdbcType="VARCHAR"/>
+            <result property="unionid" column="unionid" jdbcType="VARCHAR"/>
+            <result property="appId" column="app_id" jdbcType="VARCHAR"/>
+            <result property="nickname" column="nickname" jdbcType="VARCHAR"/>
+            <result property="avatarUrl" column="avatar_url" jdbcType="VARCHAR"/>
+            <result property="gender" column="gender" jdbcType="TINYINT"/>
+            <result property="country" column="country" jdbcType="VARCHAR"/>
+            <result property="province" column="province" jdbcType="VARCHAR"/>
+            <result property="city" column="city" jdbcType="VARCHAR"/>
+            <result property="language" column="language" jdbcType="VARCHAR"/>
+            <result property="sessionKey" column="session_key" jdbcType="VARCHAR"/>
+            <result property="accessToken" column="access_token" jdbcType="VARCHAR"/>
+            <result property="refreshToken" column="refresh_token" jdbcType="VARCHAR"/>
+            <result property="expiresIn" column="expires_in" jdbcType="INTEGER"/>
+            <result property="mobile" column="mobile" jdbcType="VARCHAR"/>
+            <result property="email" column="email" jdbcType="VARCHAR"/>
+            <result property="status" column="status" jdbcType="TINYINT"/>
+            <result property="lastLoginTime" column="last_login_time" jdbcType="TIMESTAMP"/>
+            <result property="lastLoginIp" column="last_login_ip" jdbcType="VARCHAR"/>
+            <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
+            <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        wechat_id,openid,unionid,
+        app_id,nickname,avatar_url,
+        gender,country,province,
+        city,language,session_key,
+        access_token,refresh_token,expires_in,
+        mobile,email,status,
+        last_login_time,last_login_ip,create_time,
+        update_time
+    </sql>
+</mapper>

+ 63 - 0
src/test/java/com/example/course/SocketClient.java

@@ -0,0 +1,63 @@
+package com.example.course;
+
+import java.io.*;
+import java.net.Socket;
+import java.util.Scanner;
+
+public class SocketClient {
+    public static void main(String[] args) {
+        // 客户端
+        Socket socket = null;
+        try{
+            // 连接服务端
+            socket = new Socket("127.0.0.1",1024);
+            // 获取输出流
+            OutputStream outputStream = socket.getOutputStream();
+            // 将输出流包装为打印流
+            PrintWriter printWriter = new PrintWriter(outputStream);
+            System.out.println("请输入内容");
+
+            // 创建线程,用于从控制台获取数据
+            new Thread(()->{
+                while(true){
+                    try{
+                        // 从控制台获取输入
+                        Scanner scanner = new Scanner(System.in);
+                        // 发送数据
+                        String input = scanner.nextLine();
+                        // 发送数据
+                        printWriter.println(input);
+                        // 刷新缓冲区
+                        printWriter.flush();
+                    }catch (Exception e){
+                        e.printStackTrace();
+                        break;
+                    }
+                }
+            }).start();
+
+            // 获取输入流
+            InputStream inputStream = socket.getInputStream();
+            // 将输入流包装为字符流
+            InputStreamReader reader = new InputStreamReader(inputStream,"UTF-8");
+            // 将字符流包装为缓冲流
+            BufferedReader bufferedReader = new BufferedReader(reader);
+
+            // 创建线程,用于读取服务端发送的数据
+            new Thread(()->{
+                while (true){
+                    try{
+                        // 读取数据
+                        String readData = bufferedReader.readLine();
+                        System.out.println("收到服务端消息"+readData);
+                    }catch (Exception e){
+                        e.printStackTrace();
+                        break;
+                    }
+                }
+            }).start();
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+    }
+}

+ 65 - 0
src/test/java/com/example/course/SocketService.java

@@ -0,0 +1,65 @@
+package com.example.course;
+
+import java.io.*;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * @author 11
+ */
+public class SocketService {
+    public static void main(String[] args) {
+        // 服务端
+        ServerSocket server = null;
+        Map<String,Socket> clientMap = new HashMap<>();
+        try {
+            // 绑定端口
+            server = new ServerSocket(1024);
+            System.out.println("服务已启动,等待客户端连接");
+            while(true){
+                // 阻塞等待客户端连接
+                Socket socket = server.accept();
+                // 获取客户端ip
+                String ip = socket.getInetAddress().getHostAddress();
+                System.out.println("有客户端连接ip:"+ip+"端口:"+socket.getPort());
+                String clientKey = ip+socket.getPort();
+                clientMap.put(clientKey,socket);
+                new Thread(()->{
+                    while(true){
+                        try {
+                            // 获取输入流
+                            InputStream inputStream = socket.getInputStream();
+                            // 将输入流包装为字符流
+                            InputStreamReader reader = new InputStreamReader(inputStream);
+                            // 将字符流包装为缓冲流
+                            BufferedReader bufferedReader = new BufferedReader(reader);
+                            // 读取数据
+                            String readData = bufferedReader.readLine();
+                            System.out.println("收到客户端消息->"+readData);
+
+                            clientMap.forEach((k,v)->{
+                                try{
+                                    OutputStream outputStream = v.getOutputStream();
+                                    PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(outputStream,"UTF-8"));
+                                    printWriter.println(socket.getPort() + ":" + readData);
+                                    printWriter.flush();
+                                }catch (Exception e){
+                                    e.printStackTrace();
+                                }
+                            });
+                        }catch (Exception e){
+                            e.printStackTrace();
+                            break;
+                        }
+                    }
+                }).start();
+            }
+
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+    }
+}