湘潭大学新生匿名问答网站——解湘 项目总结
湘潭大学新生匿名问答网站——解湘 项目总结
一.开发进度
大一暑假过半,7月29日建立本地工程文件
其中项目在github上经历七次push(第八次为修改配置文件,防止数据库泄露),但在本地修改次数远远大于七次。
仓库地址:Appletree24/Sky31Welcome (github.com)
后端开发均为我一人完成,前端开发由他人负责。除此之外,特感谢三翼设计部门设计出此次项目的UI界面
实际应用接口数量为31个,采用APIFOX
进行团队接口管理。部分后端实现接口实际因前端功能不需没有加入。
二.项目大纲
此项目立项起,我就很明确这是个传统的论坛类项目,砍去一些如上传图片
、用户信息修改
等不必要功能。这是一个很好的练习基于Java进行Web开发的机会,不计所谓回报,我投身到了这次的开发中。
其中因技术不牢固原因,Spring Security
框架最终并未能加入项目进行权限管理。考虑使用Shiro,但因假期繁忙,最终放弃。
其中邮件验证功能因避免网站使用的复杂性,最终删去。
C/S之间使用Nginx进行反向代理,因图片功能删去,未配置CDN加速。
三.项目中遇到的问题&收获
1.Base64编码后长度必须为4的倍数
起因为项目采用Base64对用户密码进行加盐处理,但因方式不当。导致服务端频繁报错,无法正常使用。
2.Mybatis-plus导致Mysql数据库主键出现负数
测试期间发现ORM框架生成的数据在Mysql中出现了id为负数的情况。如下图所示
MP-plus有五种主键ID生成策略:
AUTO,配合数据库设置自增主键,可以实现主键的自动增长,类型为nmber;
INPUT,由用户输入;
NONE,不设置,等同于INPUT;
ASSIGN_ID,只有当用户未输入时,采用雪花算法生成一个适用于分布式环境的全局唯一主键,类型可以是String和number;
ASSIGN_UUID,只有当用户未输入时,生成一个String类型的主键,但不保证全局唯一;
其中默认为ASSIGN_ID
,即使用雪花算法进行生成。这也就导致对应生成的主键应该使用Long类型进行存储,且设置主键类型为bigint
。也可在Java代码中设置@TableId注解
3.跨域问题
老生长谈的一个问题了。本次跨域代码如下:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//添加映射路径
registry.addMapping("/**")
//是否发送Cookie
.allowCredentials(true)
//设置放行哪些原始域 SpringBoot2.4.4下低版本使用.allowedOrigins("*")
.allowedOriginPatterns("*")
//放行哪些请求方式
.allowedMethods("GET", "POST", "PUT", "DELETE")
//.allowedMethods("*") //或者放行全部
//放行哪些原始请求头部信息
.allowedHeaders("*")
//暴露哪些原始请求头部信息
.exposedHeaders("*");
}
}
如此常见的一个问题,并不可能在开发前后端分离的项目时未进行考虑。但本次遇到的问题是,发现前端也要在vite中配置有关跨域的内容才能使cookie正常出现。
4.响应头中set-cookie
字段
- set-cookie是一个函数,由服务器向浏览器发出响应
- cookie是服务器发送给浏览器的变量
- 浏览器向服务器发送请求(put,get,post,delect方法),服务器会使用
set-cookie()方法
向本地的浏览器发送cookie,存在客户端主机中的一个文件下
set-cookie
响应头可以设置如下属性:
属性 | 意义 |
---|---|
NAME=VALUE | 赋予 Cookie 的名称和其值(必需项) |
expires=DATE | Cookie 的有效期(若不明确指定则默认为浏览器关闭前为止) |
path=PATH | 将服务器上的文件目录作为Cookie的适用对象(若不指定则默认为文档所在的文件目录) |
domain=域名 | 作为 Cookie 适用对象的域名 (若不指定则默认为创建 Cookie的服务器的域名) |
Secure | 仅在 HTTPS 安全通信时才会发送 Cookie |
HttpOnly | 加以限制, 使 Cookie 不能被 JavaScript 脚本访问 |
max-age
设置cookie的相对有效期。expire和max-age通常仅设置一个即可。比如设置max-age为1000,浏览器在添加cookie时,会自动设置它的expire为当前时间加上1000秒,作为过期时间。
如果不设置expire,又没有设置max-age,则表示会话结束后过期。
对于大部分浏览器而言,关闭所有浏览器窗口意味着会话结束。
5.用户无法发送emoji表情
Mysql中的utf8支持一个字符最多3个字节,但emoji表情为4个字节,所以需要切换为utf8mb4即可
6.敏感词过滤器
使用前缀树算法实现敏感词过滤器。
@Component
public class SensitiveFilter {
private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
private static final String REPLACEMENT = "***";
private TrieNode rootNode = new TrieNode();
@PostConstruct
public void init() {
try (
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
) {
String keyword;
while ((keyword = reader.readLine()) != null) {
this.addKeyWord(keyword);
}
} catch (IOException e) {
logger.error("敏感词加载失败: " + e.getMessage());
}
}
public String filter(String text) {
if (StringUtils.isBlank(text)) {
return null;
}
TrieNode tempNode = rootNode;
int begin = 0;
int position = 0;
StringBuilder sb = new StringBuilder();
while (begin < text.length()) {
char c = text.charAt(position);
if (isSymbol(c)) {
if (tempNode == rootNode) {
sb.append(c);
begin++;
}
position++;
continue;
}
tempNode = tempNode.getSubNode(c);
if (tempNode == null) {
sb.append(text.charAt(begin));
position = ++begin;
tempNode = rootNode;
} else if (tempNode.isEnd()) {
sb.append(REPLACEMENT);
begin = ++position;
tempNode = rootNode;
} else {
if (position < text.length() - 1) {
position++;
} else {
sb.append(text.charAt(begin));
position = ++begin;
tempNode = rootNode;
}
}
}
sb.append(text.substring(begin));
return sb.toString();
}
private boolean isSymbol(Character c) {
return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}
private void addKeyWord(String keyword) {
TrieNode tempNode = rootNode;
for (int i = 0; i < keyword.length(); i++) {
char c = keyword.charAt(i);
TrieNode subNode = tempNode.getSubNode(c);
if (subNode == null) {
subNode = new TrieNode();
tempNode.addSubNode(c, subNode);
}
tempNode = subNode;
if (i == keyword.length() - 1) {
tempNode.setEnd(true);
}
}
}
private class TrieNode {
private boolean isEnd = false;
private Map<Character, TrieNode> setNodes = new HashMap<>();
public boolean isEnd() {
return isEnd;
}
public void setEnd(boolean end) {
isEnd = end;
}
public void addSubNode(Character c, TrieNode node) {
setNodes.put(c, node);
}
public TrieNode getSubNode(Character c) {
return setNodes.get(c);
}
}
}
7.切实使用如Redis、Kafka等技术
此前只是学习过Redis、Kafka等技术,但离专业课太远,而又没有合适的项目,于是乎一直悬浮在空中,对技术一知半解。
此项目中的点赞功能、查看个人信息功能、登陆凭证存储功能、消息提醒功能均使用了Redis,也即当对象需要被频繁的访问时,我们可以使用Redis解决问题。
此项目中的消息提醒使用Kafka消息队列进行开发,在学习Kafka的过程中,再次巩固了设计模式中的发布-订阅
模式。在此抛出一个疑问,能否使用定期查询数据库的形式来实现消息提醒呢?
8.SSL证书配置
如果要使用HTTPS,那么需要配置SSL证书,在腾讯云中可白嫖免费的证书。此次采用nginx形式进行配置,只需在nginx对应配置文件中加入固定内容,并将证书文件放在置顶目录下即可,十分简单。
server {
listen 443 ssl;
server_name question.tsky31.cn;
ssl_certificate question.tsky31.cn_bundle.crt;
ssl_certificate_key question.tsky31.cn.key;
ssl_session_timeout 5m;
#请按照以下协议配置
ssl_protocols SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2;
#请按照以下套件配置,配置加密套件,写法遵循 openssl 标准。
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;
client_max_body_size 1000m;
}
server {
listen 80;
server_name question.tsky31.cn;
return 301 https://$host$request_uri;
}
9.域名配置流程
首先要申请一个域名,例如本次的tsky31.cn
,如今域名一般都在云服务提供商的后台可以进行管理,以腾讯云为例,在申请到域名后,进行对应域名的DNS解析管理,添加进项目所需要的域名,例如question.tsky31.cn
,完成解析
之后在服务器中进行nginx的配置即可。
10.统一接口形式
在接口的返回部分,最好是可以统一形式,采用创建一个类的方式来达到目的。其中进行重载,以适应不同的返回情况
package com.sky31.domain;
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> {
/**
* 状态码
*/
private Integer code;
/**
* 提示信息,如果有错误时,前端可以获取该字段进行提示
*/
private String msg;
/**
* 查询到的结果数据,
*/
private T data;
public ResponseResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public ResponseResult(Integer code, T data) {
this.code = code;
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public ResponseResult(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
}
11.tomcat日志查看
关于tomcat的日志,启动部分的成功与否,以及报错信息,存放在tomcat目录下/logs中的catalina.xxxx-xx-xx.log
文件中,可以使用cat命令查看;而成功启动后的信息,如请求接口时终端的输出,则存放在目录中的catalina.out
文件中,可以使用tail -f的命令进行查看。
12.内网穿透
因此次服务器环境较为特殊,所以跟着教程学习了内网穿透,使用了工具frp。
13.拦截器配置
关于SpringBoot的拦截器,可建一名为interceptor
的软件包,在包下进行拦截器的编写。使拦截器类实现HandlerInterceptor
这个接口,选择性重写其中的perHandle
、postHandle
、afterCompletion
方法。
之后新建配置类,实现WebMvcConfigurer
接口,在其中的addInterceptors
方法中注册编写的拦截器类即可。其中拦截器注册类拥有增加排除路径的方法。
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Autowired
private MessageInterceptor messageInterceptor;
@Autowired
private DataInterceptor dataInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.excludePathPatterns();
registry.addInterceptor(messageInterceptor)
.excludePathPatterns();
registry.addInterceptor(dataInterceptor)
.excludePathPatterns();
}
}
14.UV与DAU的统计
利用拦截器的特性。我们可以将uv与dau的统计功能使用redis+拦截器进行实现。因preHandle
方法会在请求到达dispatcherServlet前进行拦截,所以我们可以在其中获得用户的ip地址,根据ip进行UV的统计,按照年份天数等标准构建redis中的key,进而存放到redis中。
而DAU则是获取当前用户的信息,例如可以从token中提取等,判空后进行记录。
15.用户的保存
因许多接口都需要获取当前用户的身份,但每次都要用HttpServletRequest中获取,十分麻烦。如果想要在service、dao层中使用,就需要从controller层层传递。所以我们可以创建一个实用类来解决。
使用方式就是利用ThreadLocal类进行保存,将用户信息保存在线程中。浏览器每一次请求就是启动了一个线程,当请求结束,我们将用户的信息销毁即可
实现方式:
- 我们需要创建一个ThreadLocal类,创建一个ThreadLocal对象,设置ThreadLocal的set,remove,get方法
- 定义一个登录的拦截器类,实现HandlerInterceptor ,重写 preHandle() 和afterCompletion()方法 ,preHandle ()方法把登录信息写入ThreadLocal,afterCompletion()方法清除登录信息
- 我们需要设置一些配置信息,创建一个类实现 WebMvcConfigurer ,重写addInterceptors()方法,创建一个登录拦截器类的对象,给他添加到配置中,我们就实现了ThreadLocal保存用户信息
//HostHolder.java
@Component
public class HostHolder {
private ThreadLocal<User> users=new ThreadLocal<>();
public void setUser(User user){
users.set(user);
}
public User getUser(){
return users.get();
}
public void clear(){
users.remove();
}
}
//在拦截器中的preHandle进行用户信息的获取
@Component
public class DataInterceptor implements HandlerInterceptor {
public DataInterceptor() {
}
@Autowired
private DataService dataService;
@Autowired
private HostHolder hostHolder;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//统计UV
String ip = request.getRemoteHost();
dataService.recordUV(ip);
//统计DAU
User user = hostHolder.getUser();
if (user!=null){
dataService.recordDAU(user.getId());
}
return true;
}
}
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) {
//此方法是获取登录信息,登录方式不一样获取方法不一样,用户信息保存用的UserInfoVO,里边具体信息自己定义即可
UserInfoVO userInfo = getUserInfo(request);
ThreadLocalUser.set(userInfo);
return true;
}
@Override
public void afterCompletion(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler, Exception ex) {
ThreadLocalUser.clear();
}
}
16.环境配置
本项目使用到了Redis、ElasticSearch、Kafka等,其中不可避免地在服务器环境搭建时会遇到各种各样的报错,这里进行一个总结。
Redis:
- 一定要为服务器的redis设置密码,有不怀好意之人扫到6379端口后会放置挖矿病毒或是清空redis
- redis的配置中要关闭保护模式,且redis默认无法远程连接,要更改bind设置。可选择注释掉
- 在启动redis时,要使用上级目录中配置好的config文件,且可以使用-d参数后台运行
Kafka:
- 在运行Kafka前,要先运行zookeeper,可以将这两个设置为系统的服务。并且有先后级关系
- Kafka运行前,一定要在配置文件中进行集群的配置
ElasticSearch:
- ElasticSearch具有网页管理工具,名为es-head,可以进行配置
- ElasticSearch版本之间差别较大。本次使用为7.6版本。
- 通常会因为系统文件设置问题导致ES无法正常启动,要去修改/etc/profile文件
- ElasticSearch因安全原因不允许以root用户身份启动,要新建用户并chown -R $username: $directoryname
- [总结—elasticsearch启动失败的几种情况及解决_BIGmustang的博客-CSDN博客_elasticsearch无法启动](https://blog.csdn.net/BIGmustang/article/details/108585871#:~:text=总结—elasticsearch启动失败的几种情况及解决1、使用root用户启动失败在有一次搭建elasticsearch的时候,使用systemctl启动elasticsearch失败,然后在bin目录下面去使用启动脚本启动,发现报错不能用root用户启动,报“Caused by%3A java.lang.RuntimeException%3A can not,run elasticsearch as root”:[root%40localhost bin]%23.%2Felasticsearch[2017-12-)
17.maven
本项目构建工具选择了maven,关于pom.xml不再进行赘述。
在服务端进行maven打包时,可以使用mvn clean package -Dmaven.test.skip=true
来跳过测试类进行打包
18.Linux命令
记录一些项目部署中经常使用的命令:
- ps -ef |grep tomcat 在运行的进程中查找tomcat 之后可利用pid进行kill
- kill -9 pid
- tail -f $filename
19.统一处理报错
当项目规模逐渐增大时,报错的处理就成为了一个问题。我们可以使用一个类来统一处理报错
@ControllerAdvice(annotations = Controller.class)
public class MyControllerAdvice {
private static final Logger logger= LoggerFactory.getLogger(MyControllerAdvice.class);
@ExceptionHandler(Exception.class)
@ResponseBody
public void handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
logger.error("服务器发生异常"+e.getMessage());
for (StackTraceElement element:e.getStackTrace()){
logger.error(element.toString());
}
String requestHeader = request.getHeader("x-requested-with");
if (requestHeader.equals("XMLHttpRequest")){
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer=response.getWriter();
writer.write(Md5AndJsonUtil.getJSONString(1,"服务器异常"));
}else{
response.sendRedirect(request.getContextPath()+"/error");
}
}
}
20.git
起初打算采用团队形式进行开发。但因特殊原因,后端实际情况仅我一人。但仓库是已经配好的。这次学习到了使用.gitignore文件进行非上传文件的设置,只上传必要的文件,使其他用户方便使用。
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
21.application.yaml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/****?characterEncoding=utf-8&userSSL=false
username: root
password:
redis:
host: localhost
port: 6379
password:
database: 0
kafka:
bootstrap-servers: localhost:9092
consumer:
group-id: test-consumer-group
enable-auto-commit: true
auto-commit-interval: 3000
task:
execution:
pool:
core-size: 5
max-size: 15
queue-capacity: 100
scheduling:
pool:
size: 5
elasticsearch:
uris: localhost:9200
mybatis-plus:
global-config:
db-config:
id-type: none
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
22.chmod
近期因新生返校,并且由于不是所有用户都会具备校园网内网环境的问题,我需要将项目部署运行在生产机上。虽说早有心理准备生产机的环境配置会遇到与之前不一样的问题,但还是没想到有这么多。从chmod
开始一一罗列如下:
首先因生产机安全考虑,我们并不能拥有root用户,而是以开放了一定权限的普通用户作为操作者。在使用sudo rz -E
命令上传文件后,默认权限为644
状态,这就会导致几乎所有的环境部署都无法实现了,我们使用chmod 755 -R $directoryname
递归修改文件夹下的文件权限,这样才可以使得如nginx代理访问静态资源报403
、部分框架无法正常拉起目录内文件导致无法启动
等问题不再发生
23.elasticsearch-head报错406
首先分析406,开头是4,问题发生在客户端部分,着重考虑是否是客户端出了问题。406为客户端无法解析服务端发送回的数据。那么思路应该为服务端发送了什么特殊类型的数据,导致客户端无法成功解析。于是乎向着ES框架的配置文件进行搜索。
{"error":"Content-Type header [application/x-www-form-urlencoded] is not supported","status":406}
报错内容如上
查找资料后发现需要修改ES中的_site/vendor.js
文件,修改内容如下:
首先
docker exec -it $container's hashcode or name /bin/bash
进入容器内部
apt-get update
更新apt,防止安装失败
apt-get install vim
安装vim
vim /_site/vendor.js
修改文件修改 vendor.js 共有两处,重启head插件
vi _site/vendor.js6886行
contentType: "application/x-www-form-urlencoded
改成
contentType: “application/json;charset=UTF-8”
7573行
var inspectData = s.contentType === “application/x-www-form-urlencoded” &&
修改为
var inspectData = s.contentType === “application/json;charset=UTF-8” &&
24.elasticsearch删除索引内数据,但不删除索引结构
因为换到了新机器上,但是为了不出现大问题,就将测试机配置好的ES移动到了生产机,那么至少要解决的问题就是清空掉在测试机上存储的问题内容,以免影响实际的搜索功能
因为安装了es-head,所以可以在网页端进行操作
起初并未在docker拉取插件的镜像,所以想着能否使用apifox这类工具,但可惜自己的服务器很容易,但不知为何学校特殊网络环境下的服务器就连接不上了。无奈只得启用插件在web端进行解决
curl -X POST
总感觉也可,但还没学习到如何发送json
总之最后的解决方案如下
- 在网页端输入自己要删除的索引,最终应该是这种形式——
$indexname/_delete_by_query
{ "query":{ "match_all":{ } } }
25.MVC设计模式
这个没啥好写的,就是重新温习了一下MVC设计模式罢了。但是挺有意义,写在这里吧
26.Nginx+docker
因为这次的环境部署都是我负责,所以我又要去学习了nginx和docker这两样东西。
一个意外是以往很多届成员写的nginx配置竟然存在一些问题,导致部署的时候耽误了一些时间。
这次也是终于明白了Nginx为什么叫反向代理,它应该如何配置文件。docker里的image和container到底是什么关系,人们都说docker香,那么到底好在哪里?
27.注意外部包完整性
有这么一个问题。因为另外有一个非常小的项目是他人开发,我负责部署,但是我用的是2.7.2
的springboot,而他用的是2.7.3
,导致他的项目在服务端tomcat运行时找不到2.7.3
的外部包,于是便无法运行,只能访问静态资源,接口无法调用。
28.maven安装
其实在maven的使用过程中,服务器也遇到很多配置问题,但是好像解决的太顺利了,现在不记得了……
索性贴一个安装教程吧 CentOS7——安装配置Maven(Apache Maven)_Hern(宋兆恒)的博客-CSDN博客_centos maven安装与配置
29.Tomcat 404
遇到一个很恼人的问题,Tomcat里的日志文件启动是没有报错的,因为404导致也无法分析.out文件,所以就一直在猜到底是哪里的问题
网上很容易可以搜到一些常见的解决方式,我这次碰见的是自己一个疏忽忘记掉的。
记得检查一下项目文件中有没有配置serverletinitializer这个类
/**
* @AUTHOR Zzh
* @DATE 2022/8/8
* @TIME 23:27
*/
public class CommunityServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(Sky31WelcomeApplication.class);
}
}
三.写在最后
虽然写了很多代码,加起来删删改改的也得有几千行了。如果加一些小功能,希望能冲着1W行的目标写一下。给学校部门写这个网站是没有所谓的什么钱或者分的,有时聊天也是笑着说搬砖罢了。有时对于参与本次项目的所有人(真正付出了的人,混子另当别论吧)感到可惜,虽然技术确实锻炼到了,但没有物质上的回报还是令人觉得五味杂陈。还是对组内的小伙伴们再次致谢。
关于用户问题,因为这个项目是学校老师要求写的,虽然大家都知道竞品很多,但是也没办法。(甚至前几天还和同学赌用户会不会超过100
写这个项目感觉很累,复盘时思考一个原因也是因为学生组织的人数稀缺,而稀缺的人中搞技术的又参差不齐,有些时候甚至除了你没人能干活,导致开发之外的部署、日常运维都要你来干。这就很不合理了,鲜有人意识到吧……
对于工作的话,我是希望要么你就别开始,要么开始就立马结束。项目编写的时候经常一天从早到晚都在电脑前坐着,如果父母不带我出去转转,可能一天就没有什么其他活动了。还是要注意身体啊
本次项目的一大遗憾是因工时原因,最终没有使用SpringSecurity,会在之后研究加入
项目起初想过使用Go来编写,但最终因框架不熟悉而放弃。希望下一个项目能用Go
本次项目一定程度上参考了牛客社区
在19年来看,这样一个项目可以被拿来当作简历上的项目来宣传,但在如今,这样的项目也早已烂大街了。
这样也算是写过一个完整的Web开发项目,也在实验室写过了数据处理类的项目。十分圆满,总算能让浮躁的心静下来一点,之后还是着手408的学习吧。
在项目的编写过程中,遇到很多毛躁的时刻,有时因服务器环境配置需要推翻重来,有时因数据结构的复杂不知如何下手。导致有时自言自语说了很多负能量的话,感谢和我合作的同学以及父母的鼓励
目前打算使用Go手写一个RPC框架,或是尝试在Windows平台下使用DLL进行多语言合作编写一个程序,不再懊恼于大一学了很多语言的语法,却没有深入过这一点。
2022.9.2 深夜