├── .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 | 
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 |
--------------------------------------------------------------------------------