├── .gitignore ├── Dockerfile ├── model ├── checkDupID.go ├── checkDupName.go ├── createMember.go ├── findMember.go ├── updateName.go └── updateImage.go ├── docker-compose.yml ├── go.mod ├── lib ├── createFile.go └── jwt.go ├── database ├── member.go └── connection.go ├── controller ├── getInfo.go ├── patchName.go ├── signIn.go ├── signUp.go ├── putImage.go └── chattingserver │ └── SocketServer.go ├── makefile ├── config └── config.go ├── main.go ├── README.MD └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | image/ 2 | controller/chattingserver/TCP/ 3 | config/config.toml 4 | docker.env -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14.2 2 | 3 | LABEL Author="jjmin321@naver.com" 4 | 5 | COPY . src 6 | 7 | WORKDIR /go/src 8 | 9 | CMD [ "go", "run", "main.go"] 10 | 11 | EXPOSE 8080 12 | -------------------------------------------------------------------------------- /model/checkDupID.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "RandomChatting_Server/database" 4 | 5 | // CheckDupID - 멤버 중복 확인 6 | func CheckDupID(id string) error { 7 | Member := &database.Member{} 8 | err := database.DB.Where("ID = ?", id).Find(Member).Error 9 | return err 10 | } 11 | -------------------------------------------------------------------------------- /model/checkDupName.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "RandomChatting_Server/database" 4 | 5 | // CheckDupName - 멤버 중복 확인 6 | func CheckDupName(name string) error { 7 | Member := &database.Member{} 8 | err := database.DB.Where("Name = ?", name).Find(Member).Error 9 | return err 10 | } 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | postgres: 4 | image: postgres:12 5 | container_name: postgres 6 | environment: 7 | - POSTGRES_DB=${DB} 8 | - POSTGRES_USER=${DBUSER} 9 | - POSTGRES_PASSWORD=${PASSWORD} 10 | ports: 11 | - '${IP}:${POSTGRESQL}' -------------------------------------------------------------------------------- /model/createMember.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "RandomChatting_Server/database" 4 | 5 | // CreateMember - 멤버 생성 6 | func CreateMember(id, name, pw string) error { 7 | Member := &database.Member{ID: id, Name: name, Pw: pw} 8 | err := database.DB.Create(Member).Error 9 | return err 10 | } 11 | -------------------------------------------------------------------------------- /model/findMember.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "RandomChatting_Server/database" 4 | 5 | // FindMember : 멤버 찾기 6 | func FindMember(ID, Pw string) (*database.Member, error) { 7 | Member := &database.Member{} 8 | err := database.DB.Where("ID = ? AND Pw = ?", ID, Pw).Find(Member).Error 9 | return Member, err 10 | } 11 | -------------------------------------------------------------------------------- /model/updateName.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "RandomChatting_Server/database" 5 | ) 6 | 7 | // UpdateName : 닉네임 수정 8 | func UpdateName(ID, Name string) error { 9 | Member := &database.Member{} 10 | err := database.DB.Model(Member).Where("id = ?", ID).Update("name", Name).Error 11 | return err 12 | } 13 | -------------------------------------------------------------------------------- /model/updateImage.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "RandomChatting_Server/database" 5 | ) 6 | 7 | // UpdateImage : 멤버 찾기 8 | func UpdateImage(Name, Pw, fileName string) error { 9 | Member := &database.Member{} 10 | err := database.DB.Model(Member).Where("Name = ? AND Pw = ?", Name, Pw).Update("image", fileName).Error 11 | return err 12 | } 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module RandomChatting_Server 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 7 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 8 | github.com/gorilla/websocket v1.4.2 9 | github.com/jinzhu/gorm v1.9.16 10 | github.com/labstack/echo v3.3.10+incompatible 11 | github.com/labstack/gommon v0.3.0 // indirect 12 | github.com/valyala/fasttemplate v1.2.1 // indirect 13 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /lib/createFile.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "mime/multipart" 5 | "os" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | type createFileMethod interface { 11 | CreateFile() 12 | } 13 | 14 | // CreateFile - 파일 생성 15 | func CreateFile(Name string, file *multipart.FileHeader) (*os.File, string, error) { 16 | t := time.Now() 17 | now := strconv.Itoa(int(t.Unix())) 18 | fileName := Name + now + file.Filename 19 | dst, err := os.Create("image/" + fileName) 20 | return dst, fileName, err 21 | } 22 | -------------------------------------------------------------------------------- /database/member.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import "time" 4 | 5 | // Member 멤버 관리 테이블 6 | type Member struct { 7 | Idx uint `gorm:"primary_key; auto_increment:true" json:"idx"` 8 | ID string `gorm:"type:varchar(255);not null; unique;" json:"id"` 9 | Name string `gorm:"type:varchar(255);not null; unique;" json:"name"` 10 | Pw string `gorm:"type:varchar(255);not null" json:"pw"` 11 | Image string `gorm:"type:varchar(255);" json:"image"` 12 | Megaphone uint `gorm:"not null" sql:"DEFAULT:0" json:"megaphone"` 13 | JoinedAt time.Time `gorm:"not null" sql:"DEFAULT:current_timestamp" json:"joined_at"` 14 | } 15 | -------------------------------------------------------------------------------- /controller/getInfo.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "RandomChatting_Server/model" 5 | 6 | "github.com/labstack/echo" 7 | ) 8 | 9 | type getInfoMethod interface { 10 | GetInfo() 11 | } 12 | 13 | // GetInfo - 유저 정보 읽기 API 14 | func GetInfo(c echo.Context) error { 15 | ID := c.Get("ID").(string) 16 | Pw := c.Get("Pw").(string) 17 | Member, err := model.FindMember(ID, Pw) 18 | if err != nil { 19 | return c.JSON(500, map[string]interface{}{ 20 | "status": 500, 21 | "message": "멤버 조회 실패", 22 | "Member": nil, 23 | }) 24 | } 25 | return c.JSON(200, map[string]interface{}{ 26 | "status": 200, 27 | "message": "멤버 조회 완료", 28 | "Member": Member, 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # dockerfile 빌드 2 | .PHONY: build 3 | build: 4 | @docker build --tag randomchatting_server . 5 | 6 | # 서버 컨테이너 실행, 접속 7 | .PHONY: run 8 | run: 9 | @docker run -i -t -p 8080:8080/tcp --name server randomchatting_server 10 | 11 | # 실행된 dockerfile 컨테이너, 이미지 삭제 12 | .PHONY: rm 13 | rm: 14 | @docker rm server 15 | @docker rmi randomchatting_server 16 | 17 | # docker-compose.yml 서비스 시작 18 | .PHONY: compose-up 19 | compose-up: 20 | @docker-compose --env-file docker.env -f docker-compose.yml up 21 | 22 | # docker-compose.yml 서비스 삭제 23 | .PHONY: compose-down 24 | compose-down: 25 | @docker-compose --env-file docker.env -f docker-compose.yml down 26 | 27 | # 데이터베이스 컨테이너 접속 28 | .PHONY: postgresql 29 | pg: 30 | @docker exec -it postgres psql -Ujejeongmin 31 | -------------------------------------------------------------------------------- /controller/patchName.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "RandomChatting_Server/model" 5 | 6 | "github.com/labstack/echo" 7 | ) 8 | 9 | // PatchNameParam - 파라미터 형식 정의 구조체 10 | type PatchNameParam struct { 11 | Name string `json:"name" form:"name" query:"name"` 12 | } 13 | 14 | // PatchName - 내 정보 변경 메서드 15 | func PatchName(c echo.Context) error { 16 | ID := c.Get("ID").(string) 17 | u := new(PatchNameParam) 18 | if err := c.Bind(u); err != nil { 19 | return err 20 | } 21 | err := model.UpdateName(ID, u.Name) 22 | if err != nil { 23 | return c.JSON(500, map[string]interface{}{ 24 | "status": 500, 25 | "message": "이미 사용중인 닉네임입니다", 26 | }) 27 | } 28 | return c.JSON(200, map[string]interface{}{ 29 | "status": 200, 30 | "message": "성공적으로 변경되었습니다.", 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /database/connection.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "RandomChatting_Server/config" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/jinzhu/gorm" 9 | ) 10 | 11 | type connectionMethod interface { 12 | Connect() 13 | } 14 | 15 | // DB - 데이터베이스 전역변수 16 | var DB *gorm.DB 17 | 18 | // Connect - 데이터베이스 구조 생성, 연결 하는 메서드 19 | func Connect() { 20 | dbConf := config.Config.DB 21 | 22 | connectOptions := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=disable", 23 | dbConf.Host, 24 | dbConf.Port, 25 | dbConf.Username, 26 | dbConf.Name, 27 | dbConf.Password) 28 | 29 | db, err := gorm.Open("postgres", connectOptions) 30 | 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | db.AutoMigrate( 36 | &Member{}, 37 | ) 38 | 39 | DB = db 40 | 41 | log.Print("[DATABASE] 연결 완료") 42 | } 43 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | 7 | "github.com/BurntSushi/toml" 8 | ) 9 | 10 | // Config - 설정 파일 11 | var Config config 12 | 13 | type configMethod interface { 14 | InitConfig() 15 | } 16 | 17 | type config struct { 18 | App app 19 | DB database `toml:"database"` 20 | } 21 | 22 | type app struct { 23 | Name string `toml:"name"` 24 | } 25 | 26 | type database struct { 27 | Name string `toml:"name"` 28 | Username string `toml:"username"` 29 | Password string `toml:"password"` 30 | Host string `toml:"host"` 31 | Port string `toml:"port"` 32 | } 33 | 34 | // InitConfig Config 데이터 초기화 35 | func InitConfig() { 36 | configBytes, err := ioutil.ReadFile("config/config.toml") 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | _, err = toml.Decode(string(configBytes), &Config) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | log.Print("[CONFIG] 환경설정 초기화") 47 | } 48 | -------------------------------------------------------------------------------- /lib/jwt.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/dgrijalva/jwt-go" 7 | "github.com/labstack/echo" 8 | ) 9 | 10 | type jwtMethod interface { 11 | CreateAccessToken() 12 | VerifyAccessToken() 13 | } 14 | 15 | // CreateAccessToken : 액세스 토큰 생성 16 | func CreateAccessToken(ID, Pw string) (string, error) { 17 | accessToken := jwt.New(jwt.SigningMethodHS256) 18 | claims := accessToken.Claims.(jwt.MapClaims) 19 | claims["ID"] = ID 20 | claims["Pw"] = Pw 21 | claims["exp"] = time.Now().Add(time.Hour * 24).Unix() 22 | t, err := accessToken.SignedString([]byte("secret")) 23 | if err != nil { 24 | return "", err 25 | } 26 | return t, nil 27 | } 28 | 29 | // VerifyAccessToken : 액세스 토큰 검증 30 | func VerifyAccessToken(next echo.HandlerFunc) echo.HandlerFunc { 31 | return func(c echo.Context) error { 32 | token := c.Get("user").(*jwt.Token) 33 | claims := token.Claims.(jwt.MapClaims) 34 | ID := claims["ID"].(string) 35 | Pw := claims["Pw"].(string) 36 | c.Set("ID", ID) 37 | c.Set("Pw", Pw) 38 | return next(c) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /controller/signIn.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "RandomChatting_Server/lib" 5 | "RandomChatting_Server/model" 6 | 7 | "github.com/labstack/echo" 8 | ) 9 | 10 | type signInMethod interface { 11 | SignIn() 12 | } 13 | 14 | // SignInParam - 파라미터 형식 정의 구조체 15 | type SignInParam struct { 16 | ID string `json:"id" form:"id" query:"id"` 17 | Pw string `json:"pw" form:"pw" query:"pw"` 18 | } 19 | 20 | // SignIn - 로그인 메서드 21 | func SignIn(c echo.Context) error { 22 | u := new(SignInParam) 23 | if err := c.Bind(u); err != nil { 24 | return err 25 | } 26 | _, err := model.FindMember(u.ID, u.Pw) 27 | if err != nil { 28 | return c.JSON(400, map[string]interface{}{ 29 | "status": 400, 30 | "message": "해당 정보에 맞는 유저가 없습니다", 31 | }) 32 | } 33 | accessToken, err := lib.CreateAccessToken(u.ID, u.Pw) 34 | if err != nil { 35 | return c.JSON(500, map[string]interface{}{ 36 | "status": 500, 37 | "message": "토큰 생성 중 오류", 38 | }) 39 | } 40 | return c.JSON(200, map[string]interface{}{ 41 | "status": 200, 42 | "message": "로그인 성공", 43 | "accessToken": accessToken, 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "RandomChatting_Server/config" 5 | "RandomChatting_Server/controller" 6 | "RandomChatting_Server/controller/chattingserver" 7 | "RandomChatting_Server/database" 8 | "RandomChatting_Server/lib" 9 | "net/http" 10 | 11 | _ "github.com/jinzhu/gorm/dialects/postgres" 12 | "github.com/labstack/echo" 13 | "github.com/labstack/echo/middleware" 14 | ) 15 | 16 | type mainMethod interface { 17 | main() 18 | } 19 | 20 | func main() { 21 | config.InitConfig() 22 | database.Connect() 23 | e := echo.New() 24 | e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ 25 | AllowOrigins: []string{"*"}, 26 | AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodPatch, http.MethodDelete}, 27 | })) 28 | e.Use(middleware.Recover()) 29 | e.GET("/chatting", chattingserver.Socket) 30 | e.GET("/getInfo", controller.GetInfo, middleware.JWT([]byte("secret")), lib.VerifyAccessToken) 31 | e.POST("/signIn", controller.SignIn) 32 | e.POST("/signUp", controller.SignUp) 33 | e.PUT("/putImage", controller.PutImage, middleware.JWT([]byte("secret")), lib.VerifyAccessToken) 34 | e.PATCH("/patchName", controller.PatchName, middleware.JWT([]byte("secret")), lib.VerifyAccessToken) 35 | e.Logger.Fatal(e.Start(":8080")) 36 | } 37 | -------------------------------------------------------------------------------- /controller/signUp.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "RandomChatting_Server/model" 5 | 6 | "github.com/labstack/echo" 7 | ) 8 | 9 | type signUpMethod interface { 10 | SignUp() 11 | } 12 | 13 | // SignUpParam - 파라미터 형식 정의 구조체 14 | type SignUpParam struct { 15 | ID string `json:"id" form:"id" query:"id"` 16 | Pw string `json:"pw" form:"pw" query:"pw"` 17 | Name string `json:"name" form:"name" query:"name"` 18 | } 19 | 20 | // SignUp - 회원가입 API 21 | func SignUp(c echo.Context) error { 22 | u := new(SignUpParam) 23 | if err := c.Bind(u); err != nil { 24 | return err 25 | } 26 | if u.ID == "" || u.Name == "" || u.Pw == "" { 27 | return c.JSON(400, map[string]interface{}{ 28 | "status": 400, 29 | "message": "모든 값을 입력해주세요", 30 | }) 31 | } 32 | err := model.CheckDupID(u.ID) 33 | if err == nil { 34 | return c.JSON(400, map[string]interface{}{ 35 | "status": 400, 36 | "message": "이미 사용중인 아이디입니다", 37 | }) 38 | } 39 | err = model.CheckDupName(u.Name) 40 | if err == nil { 41 | return c.JSON(400, map[string]interface{}{ 42 | "status": 400, 43 | "message": "이미 사용중인 닉네임입니다", 44 | }) 45 | } 46 | err = model.CreateMember(u.ID, u.Name, u.Pw) 47 | if err != nil { 48 | return c.JSON(500, map[string]interface{}{ 49 | "status": 500, 50 | "message": "멤버 생성 중 오류 발생", 51 | }) 52 | } 53 | return c.JSON(200, map[string]interface{}{ 54 | "status": 200, 55 | "message": "회원가입 완료", 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /controller/putImage.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "RandomChatting_Server/lib" 5 | "RandomChatting_Server/model" 6 | "io" 7 | 8 | "github.com/labstack/echo" 9 | ) 10 | 11 | type putImageMethod interface { 12 | PutImage() 13 | } 14 | 15 | // PutImage - 프로필 사진 등록 16 | func PutImage(c echo.Context) error { 17 | Name := c.Get("Name").(string) 18 | Pw := c.Get("Pw").(string) 19 | file, err := c.FormFile("image") 20 | if err != nil { 21 | return c.JSON(400, map[string]interface{}{ 22 | "status": 400, 23 | "message": "파일을 읽는 데 실패하였습니다. 다시 시도해주세요.", 24 | }) 25 | } 26 | src, err := file.Open() 27 | defer src.Close() 28 | if err != nil { 29 | return c.JSON(500, map[string]interface{}{ 30 | "status": 500, 31 | "message": "파일을 여는 데 실패하였습니다. 다시 시도해주세요.", 32 | }) 33 | } 34 | dst, fileName, err := lib.CreateFile(Name, file) 35 | defer dst.Close() 36 | if err != nil { 37 | return c.JSON(500, map[string]interface{}{ 38 | "status": 500, 39 | "message": "파일을 생성하는 데 실패하였습니다. 다시 시도해주세요.", 40 | }) 41 | } 42 | if _, err = io.Copy(dst, src); err != nil { 43 | return c.JSON(500, map[string]interface{}{ 44 | "status": 500, 45 | "message": "파일을 저장하는 데 실패하였습니다. 다시 시도해주세요.", 46 | }) 47 | } 48 | err = model.UpdateImage(Name, Pw, fileName) 49 | if err != nil { 50 | return c.JSON(500, map[string]interface{}{ 51 | "status": 500, 52 | "message": "데이터베이스에 저장하는 데 실패하였습니다. 다시 시도해주세요.", 53 | }) 54 | } 55 | return c.JSON(200, map[string]interface{}{ 56 | "status": 200, 57 | "message": "프로필 사진 등록에 성공하셨습니다.", 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # 대소고 랜덤채팅 RandomChatting 2 | 3 | 💬 대구소프트웨어고등학교 학생들이 함께 사용할 수 있는 채팅 서비스입니다 4 | 5 | ![dgchat-mockup](https://user-images.githubusercontent.com/52072077/112093760-5e3cac80-8bdd-11eb-8224-f940176b7b8e.png) 6 | 7 | ## 기술 Stack 8 | | | Web | Server | 9 | |:--------------------:|:---------------:|:------------------:| 10 | | Developer | 제정민 | 제정민 | 11 | | Develop Language | VueJs| Go| 12 | | Develop Tool | Visual Studio Code | Visual Studio Code | 13 | 14 | ## 느낀점 15 | 16 | 처음으로 혼자서 만든 웹 서비스였습니다. 실제로 사용자들이 사용할 서비스라는 점을 고려하면서 개발하다보니 개발을 끝내고 나서도 추가, 보완해야할 부분들이 많았습니다. 17 | 18 | 개발부터 배포까지 하면서 힘들었지만 이 프로젝트를 진행하면서 정말 많은 기술들을 접하였고 배우게 된 것 같습니다. 19 | 20 | 21 | ## 소개 22 | 23 | 대구소프트웨어고등학교 학생들이 함께 사용할 수 있는 채팅 서비스입니다. 24 | 25 | 채팅 서비스를 만들어보고 싶었고, 닉네임을 통한 익명성이 보장되는 전체 채팅과 랜덤채팅을 구상하였습니다. 26 | 27 | 제가 원하는 기능들을 추가하고자 개인으로 제작하게 되었습니다. 28 | 29 | ## 기능 30 | 31 | ### 채팅방 입장 32 | 33 | 채팅방입장 34 | 35 | 채팅방 입장 전 화면입니다. 36 | 37 | 한 계정으로 다중 접속을 시도할 시 접속되어 있는 모든 계정의 연결이 종료됩니다. 38 | 39 | ### 랜덤 채팅 40 | 41 | 랜덤채팅 42 | 43 | 랜덤 채팅은 1대1 채팅으로 이루어지며 상대방이 자동으로 배정됩니다. 44 | 45 | 왼쪽 채팅방 목록에서 클릭을 통해 랜덤 채팅방과 전체 채팅방을 이동할 수 있습니다. 46 | 47 | ### 전체 채팅 48 | 49 | 전체채팅 50 | 51 | 전체 채팅은 접속되어 있는 모든 유저들과 채팅이 이루어지며 오른쪽 사람 목록을 통해 접속되어 있는 사람들의 목록을 확인할 수 있습니다. 52 | 53 | ### 추가적인 기능 54 | 55 | 안내사항 56 | 57 | 채팅 길이를 100자로 제한하였으며, 도배 방지를 위해 채팅을 친 후 1초동안 채팅을 보내지 못하게 막았습니다. 58 | 59 | 또한 같은 유저가 두 번 이상 입장하는 것을 막기 위하여 다중 접속을 시도할 시 그 유저의 IP를 통해 연결을 강제로 종료시킵니다. 60 | 61 | ### 그 외 62 | 63 | [jjmin321/RandomChatting_Web](https://github.com/jjmin321/randomchatting_web) 64 | 65 | [도커로 배포하기 2020-12-06](https://jjmin321.github.io/development/도커로-배포하기/) 66 | 67 | 68 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 4 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 7 | github.com/dgrijalva/jwt-go v1.0.2 h1:KPldsxuKGsS2FPWsNeg9ZO18aCrGKujPoWXn2yo+KQM= 8 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 9 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 10 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 11 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 12 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 13 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 14 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 15 | github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= 16 | github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= 17 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 18 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 19 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 20 | github.com/labstack/echo v1.4.4 h1:1bEiBNeGSUKxcPDGfZ/7IgdhJJZx8wV/pICJh4W2NJI= 21 | github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= 22 | github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= 23 | github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= 24 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 25 | github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= 26 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 27 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 28 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 29 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 30 | github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= 31 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 32 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 33 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 34 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 35 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 36 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 37 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 38 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 39 | github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= 40 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 41 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 42 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 43 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 44 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= 45 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 46 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 47 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 48 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 49 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 50 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= 51 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 52 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 53 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 54 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 58 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 59 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 60 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 61 | -------------------------------------------------------------------------------- /controller/chattingserver/SocketServer.go: -------------------------------------------------------------------------------- 1 | package chattingserver 2 | 3 | import ( 4 | "container/list" 5 | "log" 6 | "net/http" 7 | "runtime/debug" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/gorilla/websocket" 13 | "github.com/labstack/echo" 14 | ) 15 | 16 | // socketServerMethod - 구현되어 있는 메서드 모음 17 | type socketServerMethod interface { 18 | init() 19 | Socket() 20 | RecoverServer() 21 | HandleConnection() 22 | HandleClient() 23 | SendJoinMsgToClient() 24 | GetUserList() 25 | SendMsgToClient() 26 | SendMsgToRoomClients() 27 | SendMsgToAllClients() 28 | AllocateEmptyRoom() 29 | RecvMsgFromClient() 30 | DeleteFromList() 31 | DupUserCheck() 32 | } 33 | 34 | const ( 35 | // LOGIN : Magic Number for Chatting Login 36 | LOGIN = "1" 37 | // ROOMCHAT : Magic Number for Room Chatting 38 | ROOMCHAT = "2" 39 | // ALLCHAT : Magic Number for All Chatting 40 | ALLCHAT = "3" 41 | // MAXUSER : Magic Number for Chatting room max user 42 | MAXUSER = 2 43 | // MAXCOUNT : Magic Number for Chatting room max count 44 | MAXCOUNT = 50 45 | ) 46 | 47 | // Client - 채팅을 이용하는 사용자의 정보 48 | type Client struct { 49 | ws *websocket.Conn 50 | roomChatting chan string 51 | allChatting chan string 52 | quit chan int 53 | name string 54 | room *Room 55 | } 56 | 57 | // Room - 채팅방 정보 58 | type Room struct { 59 | num int 60 | clientlist *list.List 61 | } 62 | 63 | var ( 64 | // Roomlist - 이중 링크드 리스트 65 | Roomlist *list.List 66 | // UserList - 채팅에 참여하고 있는 전체 멤버 목록 67 | UserList []string 68 | // Upgrader - http 프로토콜을 ws 프로토콜로 바꿈 69 | Upgrader = &websocket.Upgrader{ 70 | ReadBufferSize: 1024, 71 | WriteBufferSize: 1024, 72 | CheckOrigin: func(r *http.Request) bool { 73 | return true 74 | }, 75 | } 76 | ) 77 | 78 | // Init - 50개 채팅방 초기화 79 | func init() { 80 | Roomlist = list.New() 81 | for i := 0; i < MAXCOUNT; i++ { 82 | room := &Room{i + 1, list.New()} 83 | Roomlist.PushBack(*room) 84 | } 85 | } 86 | 87 | // Socket - 클라이언트가 접속한 HTTP 프로토콜을 WebSocket 프로토콜로 업그레이드 시킨 후, HandleConnection 쓰레드 시작. 88 | func Socket(c echo.Context) error { 89 | ws, err := Upgrader.Upgrade(c.Response(), c.Request(), nil) 90 | if err != nil { 91 | log.Print("error occured! : ", err.Error()) 92 | return nil 93 | } 94 | go HandleConnection(ws) 95 | return nil 96 | } 97 | 98 | // RecoverServer - 소켓 통신 중 고루틴에서 발생하는 예상치 못한 에러를 무시해준다. 99 | func RecoverServer() { 100 | socketErr := recover() 101 | if socketErr != nil { 102 | log.Print("Recovered", socketErr) 103 | debug.PrintStack() 104 | } 105 | } 106 | 107 | // HandleConnection - 클라이언트를 객체를 생성한 후, HandleClient 쓰레드 호출 108 | func HandleConnection(ws *websocket.Conn) { 109 | defer RecoverServer() 110 | roomChatting := make(chan string) 111 | allChatting := make(chan string) 112 | quit := make(chan int) 113 | client := &Client{ws, roomChatting, allChatting, quit, "익명", &Room{-1, list.New()}} 114 | go HandleClient(client) 115 | } 116 | 117 | // HandleClient - RecvMsgFromClient 쓰레드를 호출하고, 클라이언트의 명령이 오면 메세지 전송, 채팅 종료 구문을 실행시킨다. 118 | func HandleClient(client *Client) { 119 | defer RecoverServer() 120 | for { 121 | select { 122 | case roomMsg := <-client.roomChatting: 123 | SendMsgToRoomClients(client.room, client.name, roomMsg) 124 | 125 | case allMsg := <-client.allChatting: 126 | SendMsgToAllClients(client.name, allMsg) 127 | 128 | case <-client.quit: 129 | client.DeleteFromList(false) 130 | return 131 | 132 | default: 133 | go RecvMsgFromClient(client) 134 | time.Sleep(1000 * time.Millisecond) 135 | } 136 | } 137 | } 138 | 139 | // RecvMsgFromClient - 클라이언트에서 명령이 올 때까지 대기하다가 명령이 오면 채널을 통해 HandleClient에게 값을 전달, 방이 모두 찼거나 한 아이디가 두 번 이상 접속한 경우 연결을 종료시킴. 140 | func RecvMsgFromClient(client *Client) { 141 | defer RecoverServer() 142 | _, bytemsg, err := client.ws.ReadMessage() 143 | if err != nil { 144 | client.quit <- 0 145 | return 146 | } 147 | msg := string(bytemsg) 148 | 149 | strmsgs := strings.Split(msg, "ᗠ") 150 | 151 | switch strmsgs[0] { 152 | case LOGIN: 153 | client.name = strings.TrimSpace(strmsgs[1]) 154 | 155 | if client.DupUserCheck() { 156 | client.ws.WriteMessage(websocket.TextMessage, []byte("접속중ᗠ"+client.name)) 157 | client.DeleteFromList(true) 158 | return 159 | } 160 | 161 | room := AllocateEmptyRoom() 162 | if room.num < 1 { 163 | client.ws.Close() 164 | } 165 | client.room = room 166 | 167 | log.Printf("%s님이 %d번째 방에 입장하셨습니다\n", client.name, client.room.num) 168 | 169 | client.ws.WriteMessage(websocket.TextMessage, []byte("방 번호ᗠ"+strconv.Itoa(client.room.num))) 170 | client.SendJoinMsgToClient() 171 | client.GetUserList() 172 | room.clientlist.PushBack(*client) 173 | 174 | case ROOMCHAT: 175 | client.roomChatting <- strmsgs[1] 176 | 177 | case ALLCHAT: 178 | client.allChatting <- strmsgs[1] 179 | } 180 | } 181 | 182 | // SendJoinMsgToClient - 클라이언트가 연결되면 모든 사람에게 접속했다고 메세지 보냄, 방에 접속한 메세지는 같은 방 사람에게만 보냄 183 | func (client *Client) SendJoinMsgToClient() { 184 | if client.name == "익명" { 185 | return 186 | } 187 | allJoinMsg := "전체 유저 접속ᗠ" + client.name 188 | roomJoinMsg := "방 유저 접속ᗠ" + client.name 189 | client.ws.WriteMessage(websocket.TextMessage, []byte(allJoinMsg)) 190 | client.ws.WriteMessage(websocket.TextMessage, []byte(roomJoinMsg)) 191 | for re := Roomlist.Front(); re != nil; re = re.Next() { 192 | r := re.Value.(Room) 193 | for e := r.clientlist.Front(); e != nil; e = e.Next() { 194 | c := e.Value.(Client) 195 | c.ws.WriteMessage(websocket.TextMessage, []byte(allJoinMsg)) 196 | if client.room.num == c.room.num { 197 | c.ws.WriteMessage(websocket.TextMessage, []byte(roomJoinMsg)) 198 | } 199 | } 200 | } 201 | } 202 | 203 | // GetUserList - 접속되어 있는 사람들의 정보를 반환함 204 | func (client *Client) GetUserList() { 205 | var allUser string 206 | var roomUser string 207 | for re := Roomlist.Front(); re != nil; re = re.Next() { 208 | r := re.Value.(Room) 209 | for e := r.clientlist.Front(); e != nil; e = e.Next() { 210 | c := e.Value.(Client) 211 | if client.name != c.name { 212 | allUser = c.name 213 | client.ws.WriteMessage(websocket.TextMessage, []byte("전체 유저 정보ᗠ"+allUser)) 214 | } 215 | if client.room.num == c.room.num { 216 | roomUser = c.name 217 | client.ws.WriteMessage(websocket.TextMessage, []byte("방 유저 정보ᗠ"+roomUser)) 218 | } 219 | } 220 | } 221 | } 222 | 223 | // SendMsgToClient - 클라이언트에게 웹소켓을 통해 메세지를 전송 224 | func SendMsgToClient(client *Client, sender string, msg string, all bool) { 225 | if all == true { 226 | chatting := "전체채팅ᗠ" + sender + "ᗠ" + msg 227 | client.ws.WriteMessage(websocket.TextMessage, []byte(chatting)) 228 | } else { 229 | chatting := "랜덤채팅ᗠ" + sender + "ᗠ" + msg 230 | client.ws.WriteMessage(websocket.TextMessage, []byte(chatting)) 231 | } 232 | } 233 | 234 | // SendMsgToRoomClients - 이중링크드리스트를 순회하여 클라이언트의 방 인덱스를 찾은 뒤, 방 인덱스와 메세지를 sendMsgToClient에게 전달한다. 235 | func SendMsgToRoomClients(room *Room, sender string, msg string) { 236 | for e := room.clientlist.Front(); e != nil; e = e.Next() { 237 | c := e.Value.(Client) 238 | SendMsgToClient(&c, sender, msg, false) 239 | } 240 | } 241 | 242 | // SendMsgToAllClients - 이중링크드리스트를 순회하여 존재하는 모든 클라이언트의 방 인덱스를 찾은 뒤, 방 인덱스와 메세지를 sendMsgToClient에게 전달한다. 243 | func SendMsgToAllClients(sender string, msg string) { 244 | for re := Roomlist.Front(); re != nil; re = re.Next() { 245 | r := re.Value.(Room) 246 | for e := r.clientlist.Front(); e != nil; e = e.Next() { 247 | c := e.Value.(Client) 248 | SendMsgToClient(&c, sender, msg, true) 249 | } 250 | } 251 | } 252 | 253 | // AllocateEmptyRoom - 이중링크드리스트를 오름차순으로 순회하며 유저가 2명이 아닌 리스트를 배정, 모든 방에 유저가 찼으면 접속하지 못함. 254 | func AllocateEmptyRoom() *Room { 255 | for e := Roomlist.Front(); e != nil; e = e.Next() { 256 | r := e.Value.(Room) 257 | if r.clientlist.Len() < MAXUSER { 258 | return &r 259 | } 260 | } 261 | return &Room{-1, list.New()} 262 | } 263 | 264 | // DupUserCheck - 중복되는 닉네임이 이미 채팅에 접속되어 있을 경우 입장시키지 않음. 265 | func (client *Client) DupUserCheck() bool { 266 | for re := Roomlist.Front(); re != nil; re = re.Next() { 267 | r := re.Value.(Room) 268 | for e := r.clientlist.Front(); e != nil; e = e.Next() { 269 | c := e.Value.(Client) 270 | if strings.Compare(client.name, c.name) == 0 { 271 | return true 272 | } 273 | } 274 | } 275 | return false 276 | } 277 | 278 | // DeleteFromList - 클라이언트의 접속이 끊어지면 링크드리스트에서도 삭제함. 279 | func (client *Client) DeleteFromList(isDup bool) { 280 | for re := Roomlist.Front(); re != nil; re = re.Next() { 281 | r := re.Value.(Room) 282 | for e := r.clientlist.Front(); e != nil; e = e.Next() { 283 | c := e.Value.(Client) 284 | // 중복되서 나가는 계정은 퇴장 메세지를 보내지 않음, 원래 접속되어 있는 계정은 웹에서 퇴장시키므로 상관 X 285 | if client.name != "익명" && isDup == false { 286 | c.ws.WriteMessage(websocket.TextMessage, []byte("사람 나감ᗠ"+strconv.Itoa(client.room.num)+"ᗠ"+client.name)) 287 | log.Printf("%s님에게 %s님이 퇴장하였다고 전송되었습니다", c.name, client.name) 288 | } 289 | } 290 | } 291 | for re := Roomlist.Front(); re != nil; re = re.Next() { 292 | r := re.Value.(Room) 293 | for e := r.clientlist.Front(); e != nil; e = e.Next() { 294 | c := e.Value.(Client) 295 | // if : 다중 접속 시도한 IP를 제거 , else if : 원래 접속되어 있던 게정도 제거 296 | if client.ws.RemoteAddr() == c.ws.RemoteAddr() { 297 | r.clientlist.Remove(e) 298 | } else if client.name == c.name { 299 | r.clientlist.Remove(e) 300 | c.ws.WriteMessage(websocket.TextMessage, []byte("접속중ᗠ"+c.name)) 301 | } 302 | } 303 | } 304 | log.Printf("%s님이 %d번째 방에서 퇴장하셨습니다\n", client.name, client.room.num) 305 | } 306 | --------------------------------------------------------------------------------