반응형

 

프로그램 랭귀지 처럼 변수를 만들어서 결과를 임시로 보관할 수가 있다

 

--변수 선언 , 생성함과 초기화 할수도있고 
DECLARE @i as INT = 10;

--생성만 한다음, 나중에 넣을 수도 있다
DECLARE @j as INT;
SET @j = 20;

SELECT @i,  @j;

 

실행결과 

 

 

 

 

가장 많은 역봉을 받은 사람의 이름을 담는 구문

 

결과화면

 

SELECT ~ DESC 까지 결과를 firstName 에 담고 이것을 질의한 결과다

 

 

아래 구문은 sql 서버에 한정된 문법인데 다음 처럼 편하게 여러 변수에 담을 수도 있다

 

 

 

 

 

반응형
반응형

 

 

거래의 경우

  • A 의 인벤토리에서 아이템 제거
  • B의 인벤토리에 아이템 추가
  • A의 골드 감소
    이렇게 모두 처리되야 하는데 중간에 실패가 난다면 완전히 거래가 처리 되는것이 아니고 오류가 되어버리는데
    이렇게 되면 테이블 데이터가 잘못되게 된다, 강화 하는 경우에도 비슷한 케이스
    All or Nothing 
    이런걸 해결 하기위해 TRANSACTION 이 있다
    아무것도 쓰지 않으면 기본 적으로 TRANSACTION 이 있고 그 뒤에 COMMIT 이 있는 것인데 (EX : INSERT INTO ...)
    BEGIN TRAN; 을 명시하면 뒤에 COMMIT 또는 ROLLBACK 을 적어 처리할지 되돌릴지를 정할 수 있다
INSERT INTO accounts VALUES(1, 'TESET STR', 100, GETUTCDATE());

BEGIN TRAN;
	INSERT INTO accounts VALUES(2, 'TESET STR', 100, GETUTCDATE());
ROLLBACK;

 

 

결과를 보면 두번째 것은 추가 안된것을 알수 있다 ROLLBACK 되었음으로

 

 

 

 

INSERT INTO accounts VALUES(1, 'TESET STR', 100, GETUTCDATE());

BEGIN TRAN;
	INSERT INTO accounts VALUES(2, 'TESET STR', 100, GETUTCDATE());
COMMIT;

추가된 경우

 

 

 

 

 

 

아래 구문을 실행하기 전 데이터

 

--TRY CATCH 와 비슷한 구문
BEGIN TRY
		BEGIN TRAN;
			INSERT INTO accounts VALUES(1, 'T1', 100, GETUTCDATE());
			INSERT INTO accounts VALUES(2, 'T2', 100, GETUTCDATE());
		COMMIT;
END TRY
BEGIN CATCH
	IF @@TRANCOUNT > 0
		ROLLBACK;
END CATCH

@@TRANCOUNT 는 TRAN 이 몇개 인지 알수 있는 매크로인데

BEGIN TRAN;

       TRAN;

 

이렇게 TRAN 을 중첩시키면 1이상이 될 수가 있는데 이때 이 개수를 리턴해주는게 @@TRANCOUNT 이다

 

그리고 INSERT 는 accountId 가 primary key 임으로 이미 존재하는 키가 있을 경우 또다시 추가 하려고 하면 에러가 발생되어 catch 로 잡히게 된다

 

실행후 상황

아무 영향이 없었다는 걸 알 수 있다

 

PRINT('TTT') 를 ROLLBACK 구문 쪽에 써서 ROLLBACK 된 이유를 적어줄 수도 있다

 

중요한건 TRAN  으로 묶어놓은건 한번에 다 실행하거나 아니면 중간에 오류/예외가 발생하면 모두 실행되지 않는 다는 것이다

그리고 TRANSACTION 은 보통 두개 이상의 테이블에 어떤 변경이나 영향을 줄때 사용된다

 

 

사용시 주의 할점은

TRAN 안에는 꼭 원자적으로 실행될 애들만 넣어야 한다

즉 성능적으로 문제가 될 수 있기 때문인데

LOCK이 되기 때문

 

 

만약

BEGIN TRAN;
	INSERT INTO accounts VALUES(2, 'TESET STR', 100, GETUTCDATE());

이렇게 까지 되어 있으면 위코드는 COMMIT 이나 ROLLBACK 을 만나기 전까지 계속 LOCK 리 걸린 상태가 되며

 

다른 구문에서 accounts 를 조회하려는 구문을 실행한다 해도 select 구문은 실행 되지 않고 계속 대기하게 된다

대기상태에 빠지게 됨 commit 이나 rollback 을 만나지 않는다면 원자적 특성때문에

 

 

같은 얘기지만 TRAN  과 COMMIT/ROLLBACK 사이의 구문은 길지 않게 작성하는 것이 좋다

 

 

반응형

'서버(Server) > DB' 카테고리의 다른 글

DB : BATCH : GO  (0) 2023.02.19
DB : 변수  (0) 2023.02.18
DB : INNER JOIN, CROSS JOIN  (0) 2023.02.16
DB : UNION : 합치면서 중복제거, INTERSECT 교집합, EXCEPT 차집합  (0) 2023.02.15
DB : 인덱스(Index)  (0) 2023.02.13
반응형

CROSS JOIN (교차 결합)
서로 교차를 하면서 하나씩 결합을 한다는 것
(1,A), (1,B), (1,C), (2,A)... 총 9개

CREATE TABLE testA
(
	a INTEGER
)

CREATE TABLE testB
(
	B VARCHAR(10)
)

INSERT INTO testA VALUES(1);
INSERT INTO testA VALUES(2);
INSERT INTO testA VALUES(3);

INSERT INTO testB VALUES('A');
INSERT INTO testB VALUES('B');
INSERT INTO testB VALUES('C');


SELECT *
FROM testA;

SELECT *
FROM testB;


--CROSS JOIN (교차 결합)
--서로 교차를 하면서 하나씩 결합을 한다는 것
--(1,A), (1,B), (1,C), (2,A)... 총 9개

SELECT *
FROM testA
	CROSS JOIN testB;

SELECT *
FROM testA, testB;

결과화면

 

 

 

 

 

 

 

 

 

기본 데이터 보기

USE BaseballData;


SELECT *
FROM players
ORDER BY playerID;

SELECT *
FROM salaries
ORDER BY playerID;

 

 

 


INNER JOIN(두개의 테이블을 가로로 결합 + 결합 기준을 ON 으로 한다)
UNION 은 세로 즉 위아래로 합치는 것이였다면 INNER JOIN 은 옆으로 합치는 것

SELECT *
FROM players AS P
	INNER JOIN salaries AS S
	ON P.playerID = S.playerID;

 

players 의 playerID 와 salaries 의 playerID 가 같은 행 끼리 합치는 것

주의 할점은 조건이 = 인데 양쪽 모두에 playerID 가 있어야 붙여지게 되지 한쪽이라도 id 값이 없다면 해당 행은 걸러진다

즉 양쪽에 모두 정보가 있을때만 나온다

 

결과를 보면 뒤에 추가 된걸 볼 수 있다

 

inner join 을 한다는 건 새로운 테이블을 만든 것

 

 

 

 

 

 

-- outer join (외부 결합)
--어느 한쪽에만 존재하는 데이터가 있을때 정책을 어떻게 할것인지에 대한 것
-- left join 인경우로 예를 들어보면 두개를 조인 할때 왼쪽에만 있고 오른쪽에는 없다면
--왼쪽 정보를 그대로 채워 넣고 없는 오른쪽 정보는 null 로 채워 넣어서 join 을 한다는 것으로 
--inner join 과 유사한데 비어 있는 것을 어떻게 처리 할것인가에 대한 내용이다

SELECT *
FROM players AS P
	LEFT JOIN salaries AS S
	ON P.playerID = S.playerID
	ORDER BY P.playerID;

 

 

아래는 playerID 로 정렬하여 두 테이블(players과 salaries )을 본것이고

 

 

LEFT JOIN 한다음의 모습이다 

adairbi99m 의 줄에서 끝을 보면 

 

 

salaries 에는 adairbi99m  이 없기 때문에 끝에 NULL 로 채워진것을 볼 수 있다

 

 

 

RIGHT JOIN 은 반대의 개념이 된다

정보가 오른쪽에 있다면 표시되고 그 이후에 같은 행에 왼쪽(plyaer)에 없으면 왼쪽 정보는 NULL 로 채워진다

예시 이미지

 

오른쪽에 정보는 있지만

 

왼쪽이 null 로 채워진 경우 

 

 

그런데 left 나 right 나 테이블 순서를 바꿔주면 동일한 효과가 된다

 

 

 

 

정리하자면 

cross join 은 * 이고

inner join, left join right 조인은 같은 행에 추가 하여 테이블을 만드는 것이다

 

 

반응형
반응형

 

union 시 주의할 점은 열이 같아야지 합처진다 그런데 A , B 중에서 중복은제거

UNION 은 || or 연산과 비슷한데 중복은 제거 한다

select playerID, AVG(salary)
from salaries
group by playerID
having AVG(salary) >= 3000000

UNION

--12월에 태어난 선수들의 playerID
select playerID, birthMonth
from players
where birthMonth = 12
order by playerID asc
;

 

아래는 위 결과를 실행했을때의 결과인데 avg 와 birthmonth 때문에 합쳐지지 않아

silvaca01 이 별도 있는 거을 볼 수 있다

 

 

 

 

 

 

union 열을 같게끔 해주면 중복은제거된다

 

--커리어 평균 연봉이 3000000 이상인 선수들의 playerID
select playerID
from salaries

group by playerID
having AVG(salary) >= 300000 

UNION

--12월에 태어난 선수들의 playerID
select playerID
from players
where birthMonth = 12
order by playerID asc
;

이렇게 하면 합쳐진 하나만 나오는 것을 알수 있다 ( 중복 제거 )

 

 

 

 

union all 은 중복을 허용한다

select playerID
from salaries

group by playerID
having AVG(salary) >= 300000 

UNION all

--12월에 태어난 선수들의 playerID
select playerID
from players
where birthMonth = 12
order by playerID asc
;

 

 

union 을 쓰게 되면 order by 는 가장 하단에 와야 한다

 

 

 

 

교집합(intersect) 을 구한다 즉 양쪽 모두 만족하고 존재하는것을 구한다

--교집합(intersect) 을 구한다 즉 양쪽 모두 만족하고 존재하는것을 구한다
--커리어 평균 연봉이 3000000 이상이거나 (&&) 12월에 태어난 선수들
select playerID
from salaries

group by playerID
having AVG(salary) >= 300000 

intersect

--12월에 태어난 선수들의 playerID
select playerID
from players
where birthMonth = 12
order by playerID asc
;

 

 

 

 

--차집합
--커리어 평균 연봉이 3000000 이상이거나 (-) 12월에 태어난 선수들
select playerID
from salaries

group by playerID
having AVG(salary) >= 300000 

except

--12월에 태어난 선수들의 playerID
select playerID
from players
where birthMonth = 12
order by playerID asc
;

위는 차집합으로 A - B 가 된다

명령어는 except 가 된다

 

반응형

'서버(Server) > DB' 카테고리의 다른 글

DB : TRANSACTION  (0) 2023.02.17
DB : INNER JOIN, CROSS JOIN  (0) 2023.02.16
DB : 인덱스(Index)  (0) 2023.02.13
DB : DB(스키마) 만들기, 테이블 만들기와 Primary Key 의 성능  (0) 2023.02.12
DB : SUBQUERY  (0) 2023.02.10
반응형

DB 에선 2진 검색 트리를 사용한다, 균형잡힌 트리등 각 DB 마다 다를 순 있다

 

어떤 게임에서 특정 유저 ABC 에게 귓말을 날린다 하면 다른 방법을 쓰지 않는다면 모든 유저중에서 ABC 를 찾아야 하지만 이때 index 를 사용하면 빠르다

 

유저명순으로 이름들을 정렬한 다음 여기에  index 를 매긴다

즉 트리종류의 검색으로 빠르게 찾는것이 index 의 방식이다

이때 유저명에다가 index 를 걸어 찾기를 더 빠르게 한다는 개념

 

 

 INDEX 는 두가지 종류가 있다

  1. CLUSTERED INDEX  : 테이블당 1개만 존재하고 제일 빠르다, 공간도 적게 차지함
    Primary key 대부분이 clustered index 이다
    영한사전 처럼 알파벳 순서처럼 이미 데이터가 정렬이 되어 있고 여기서 찾는 것

  2. NON-CLUSTERED INDEX : 별다른 제한 없음
    별도의 공간에 색인목록을 만들어 색인 목록으로 한번 간다음 여기서 다시 데이터를 찾는 방식
    즉 따로 관리하는 Lookup테이블로 색인을 먼저 한다음 찾는다

 

디폴트로는 Primary key 로 지정하면 Clustered Index 인데, 지정을 Non - Clustered Index  로 한다면 이것도 가능은 하지만 이렇게 사용할일은 거의 없다

 

 

 

인덱스 지정하기

CREATE INDEX i1 ON accounts(accountName);

 

 

 

 

 

인덱스를 accountname 에 지정한다음 위 그림처럼 i1 으로 accountname  에 대한 인덱스가 지정된 것을 알 수있다

 

 

DROP INDEX accounts.i1;  

이렇게 인덱스를 삭제 할 수 있다

 

accountname 을 인덱스로 지정하는 상황은 이름으로 검색을 하는 경우를 예로 들 수 있다

 

 

CREATE UNIQUE INDEX i1 ON accounts(accountName);

--중복 되는 것이 없는 인덱스

 

 

CREATE CLUSTERED INDEX i1 ON accounts(accountName);

이렇게 지정하면 CLUSTERED 인덱스로 지정을 하려고 하지만 테이블에 하나만 클러스터 인덱스만 존재 함으로 추가로 지정하려 하면 오류가 난다

 

 

 

CREATE INDEX i1 ON accounts(accountName, ballCount);

이렇게 여러개를  조합하여 묶어 인덱스로 지정하는 것도 가능하다

 

 

 

 

 

 

반응형
반응형
--DB 만들기, 스키마
create database GameDB;

--db 사용
use GameDB;


--테이블 만들기
create table accounts(
	accountId integer not null,
	accountName varchar(10) not null,
	coins integer default 0,
	createdTime DATETIME
);

select *
from accounts;

 

 

 

--테이블 삭제
--drop table accounts;

--열추가
ALTER TABLE accounts
ADD lastEnterTime DATETIME;

 

 

 

 

 

--accounts 에 accountId 를 primary 로 설정한다
 ALTER TABLE accounts
 ADD PRIMARY KEY (accountId);

 

 

Primary key 로 설정하면 

 

위 처럼 Clustered Index Seek 라고 표기 된걸 볼수 있는데 이것은 map 또는 dictionary 같은 성능을 보인다

 

하지만 Primary key 를 제거하면 

이 처럼 Table Scan 으로 바뀌게 되어 테이블을 뒤지는 형태로 느린 성능을 보이게 된다

 

 

 

반응형

'서버(Server) > DB' 카테고리의 다른 글

DB : UNION : 합치면서 중복제거, INTERSECT 교집합, EXCEPT 차집합  (0) 2023.02.15
DB : 인덱스(Index)  (0) 2023.02.13
DB : SUBQUERY  (0) 2023.02.10
DB : subquery  (0) 2023.02.04
DB : Insert into , Delete, Update  (0) 2023.02.03
반응형

--exists 존재한다면 진행하고 없다면 스킵한다
--not exists exists 와 반대

 

select *
from players
where exists (select playerID from battingpost where battingpost.playerID = players.playerID );

(select playerID from battingpost where battingpost.playerID = players.playerID )

서브 쿼리를 보면 battingpost.playerID = players.playerID 두 조건이 같은것이 존재 할때만(exists) players 의 정보를 보여주며 그렇지 않으면 스킵한다


결과

반응형

'서버(Server) > DB' 카테고리의 다른 글

DB : 인덱스(Index)  (0) 2023.02.13
DB : DB(스키마) 만들기, 테이블 만들기와 Primary Key 의 성능  (0) 2023.02.12
DB : subquery  (0) 2023.02.04
DB : Insert into , Delete, Update  (0) 2023.02.03
DB : group by  (0) 2023.02.02
반응형
select *
from players
where playerID = (select top 1 playerID from salaries order by salary desc );

playerID 에 해당 하는 내용을 하단과 같이 select 문을 다시 넣어 검색하여 이것의 결과를 활용하는 것을 보고 subquery 라 하고 두번에 나누어서 처리 할것은 한번에 처리 할 수 있다

 

주의 할것은 위 처럼 단일행은 동작 가능하지만 다중행에 대해선 동작하지 않는다

 

 

 

select *
from players
where playerID in (select top 10 playerID from salaries order by salary desc );

하지만 in 을 사용하게 되면 서브쿼리에 있는 내용을 모두 상위 where player 와 매칭 되는것을 모두 보여준다

결과 화면에 3개만 보여지는 이유는  IN 으로 처리 하면 중복되는것은 하나만 보여주기 때문이다

 

 

서브쿼리는 where 에서 많이 사용 되지만 다른 구문에서도 사용 가능하다 

 

다음 처럼 insert into 에도 서브쿼리를 넣을 수도 있다

insert into salaries
values (2023, 'KOR', 'NL', 'kkk', (select max(salary) + 100 from salaries));

 

 

 

 

테이블의 모든 모든 요소들을 다른 테이블에서 새로운 테이블에 일부요소들에 대한 복사하기를 서브쿼리와 유사한 형태로 쓸 수 있다

 

salaries_temp 를 복사를 받을 테이블

--INSERT SELECT 구문

insert into salaries_temp
select  yearID, playerID, salary from salaries;

위는 salaries 내용을 salaries_temp  에 복사하는 코드다

 

 

 

select *
from salaries_temp;

위와 같이 복사 된것을 알수 있다

 

 

원래 salaries 는 다음 처럼 요소들이 배치되어 있다

반응형

'서버(Server) > DB' 카테고리의 다른 글

DB : DB(스키마) 만들기, 테이블 만들기와 Primary Key 의 성능  (0) 2023.02.12
DB : SUBQUERY  (0) 2023.02.10
DB : Insert into , Delete, Update  (0) 2023.02.03
DB : group by  (0) 2023.02.02
DB : COUNT, DISTINCT 각종 함수들  (0) 2023.02.01
반응형
select * 
from salaries;

 

 

insert into salaries
values (2023, 'KOR', 'NL', 'killer', 100000000)

insert 할때

이렇게 컬럼 명을 쓰고 그거에 맞게 values 를 넣어도 가능하다(이때 순서가 달라고 가능하다)

 

 

delete from salaries;

테이블을 삭제한다

 

 

 

 

 

 

조건에 맞는 열을 지운다

delete from salaries
where playerID ='barkele01';

주의 할점은 조건에 맞는것을 모두 삭제한 다는 것이다

salary 중에 null 을 삭제 한다고 하면 모든 열중 null 인걸 전부 삭제한다

 

 

 

 

 

 

update 하기 전 원래 데이터

 

 

update salaries
set salary = salary * 2
where teamID = 'ATL';

update 테이블명

salary 에 두배로 곱한 쿼리문 적용 후의 결과

 

 

 

콤마(,) 로 연속적으로 값을 업데이트 할 수 있다

update salaries
set salary = salary * 2, yearID = yearID +1
where teamID = 'ATL';

 

delete, update 등 실제 쿼리를 sql server 에디터에서 실행하면 테이블 값들에 실제 값이 적용 되어 있는 것임으로

테스트 할때는 원래 데이터를 빽업해놓는것이 좋다

 

그리고 실제 데이터를 삭제 할때는 실제 삭제하지 않고 delete 열을 하나 두어 삭제할때 delete 열에 체크만해서 삭제 하는 케이스들이 많다(ex 계정 같은 경우들, 복구를 위해서)

 

 

 

반응형

'서버(Server) > DB' 카테고리의 다른 글

DB : SUBQUERY  (0) 2023.02.10
DB : subquery  (0) 2023.02.04
DB : group by  (0) 2023.02.02
DB : COUNT, DISTINCT 각종 함수들  (0) 2023.02.01
DB : Case, where  (0) 2023.01.31
반응형

원래 데이터는 이렇게 있는데

 

2004년 도에 가장 많은 홈런을 날린 팀은?

select  teamID, sum(HR) as homeRuns
from batting
where yearID=2004
group by teamID
order by homeRuns desc;

 

group by 는 공통된 것을 하나의 단위로 묶는 것인데

이 묶음으로 하나로 보려 하는 것이 group by 라서 

group by 한 것을 select * 할 수는 없다

 

즉 group 된 것의 정보에서 뭔가 SUM 이나 계량적인 데이터를 종합해 보려고 할때 유용하다

 

 

 

2004년 도에 가장 많은 홈런을 날린 팀은?

select teamID, sum(HR) as homeRuns
from batting
where yearID=2004 
group by teamID
having SUM(HR) >= 200
order by homeRuns desc;

여기서 having 을 볼 수 있는데 having 은 group by 다음에 조건을 걸 수 있는 키워드 이다

group 으로 묶은 목록들 중에서 다시 조건을 걸기 위해선 where 로는 충족되지 못하기 때문에 하나 더 있다고 볼 수 있다

 

 

 

명령문은 다음 순서대로 실행 되기 때문에 select 를 가장 최상단에 쓰는 쿼리 문에서는  select 다음에 오는 컬럼명에 올 수 있는 것인지에 대한 판단은 아래 순서를 고려해야 한다 (as 별칭 포함)

  1. from
  2. where
  3. group by
  4. having
  5. select
  6. order by

 

 

단일년도에 가장 많은 홈런을 날린 팀은?

select teamID, sum(HR) as homeRuns
from batting
group by teamID
order by homeRuns desc;

 

이렇게 볼 수 잇는데 이때는 같은 팀은 맞는데 연도가 섞여 있으면서 각 연도별에 대한 홈런을 모두 합한 것이 됨으로

 

 

 

 

만약 위 상황에서 같은 팀에서도 연도별 분류 한다면?

select teamID, yearID, sum(HR) as homeRuns
from batting
group by teamID, yearID
order by homeRuns desc;

같은 팀에서도 단일 연도로 분류하여 구 할 수 있다

 

NYA 에 대한 결과는 하단에 더 있지만 생략..

반응형

'서버(Server) > DB' 카테고리의 다른 글

DB : subquery  (0) 2023.02.04
DB : Insert into , Delete, Update  (0) 2023.02.03
DB : COUNT, DISTINCT 각종 함수들  (0) 2023.02.01
DB : Case, where  (0) 2023.01.31
DB : 날짜와 관련된 기능들 GETUTCDATE()  (0) 2023.01.30
반응형

 

 

COUNT 는 NULL 인 것은 제외하여 개수를 센다

 

DISTINCT 중복은 제거하고 나열한다

 

 

select count(birthMonth)
from players;

select distinct birthMonth
from players;

 

원래 이렇게 생긴 테이블에서 

위 코드를 실행하면 다음 처럼 된다

 

NULL 또한 distinct 대상이 된다 => 중복만 제거하는 것임으로

 

 

 

 

 

위의 코드에선 3개가 모두다 같아야 중복 처리가 된다

그렇지 않다면 위 처럼 중복 분기처리가 된다

 

 

 

그 밖의 함수들

집계 함수들에선 NULL 은 처리 하지 않고 무시한다

몸무게의 평균이 된다

 

 

 

다음 처럼 NULL 일대는 0 으로 취급하여 처리 할 수도 있다

0 이 추가 되어 평균이 낮아졌다

 

 

 

MIN, MAX 는 문자와 날짜에도 사용이 가능하다

 

 

 

 

 

 

팀이 보스턴이면서 단일 년도 최다 홈런 친 사람의 정보

select top 1 *
from batting
where teamID = 'BOS' 
order by HR DESC;

이렇게 batting 테이블에서  id 를 알아온 다음 플레이어 정보에서 playerid 를 통하여 정보를 가져와 뿌려주면 된다

 

 

반응형

'서버(Server) > DB' 카테고리의 다른 글

DB : Insert into , Delete, Update  (0) 2023.02.03
DB : group by  (0) 2023.02.02
DB : Case, where  (0) 2023.01.31
DB : 날짜와 관련된 기능들 GETUTCDATE()  (0) 2023.01.30
DB : DATETIME 날짜 넣기, 시간 관련 함수와 기능들  (0) 2023.01.28
반응형
use BaseballData;

select birthMonth
from players;

select *,
	case birthMonth
		when 1 then N'겨울'
		when 2 then N'봄'
		when 3 then N'가을'
		when 8 then N'8이다'
		else N'그밖에'
	end as birthSeason
from players;

 

switch case 와 비슷한걸 알 수 있다

 

위에서 end as birthSeason  끝에 새로 추가된 컬럼의 이름을 birthSeason 으로 지정하겠다는 얘기다

 

case 의 where 조건에 맞춰 문자로 변환되어 추가 된것을 볼 수 있다

 

 

 

 

아래 처럼 조건문을 추가 하는 구문 또한 있다

select *,
	case 
		when birthMonth <=1 then N'back'
		when birthMonth <=3 then  N'나이스'
		when birthMonth <=6 then  N'앜'
		when birthMonth <=9 then  N'9이하'
		when birthMonth <=12 then  N'12이하'
		else N'그밖에'
	end as birthSeason
from players;

 

위 구문들에서 else 구문이 없다면 else 에 에 해당 하는것ㅇ느 birthSeason 에서 NULL 이 된다

주의 할점  birthMonth = NULL 이렇게 조건문을 쓸 수 없고 birthMont is NULL 이렇게 비교를 해야한다

 

 

 

 

반응형
반응형

시간을 얻어올때 UTC 로 통일된 시간으로 처리하면 어느 나라든지 상관 없이 기준이 되는 UTC 시간으로 처리 할수 있기 때문에 시간계산에 혼선을 줄일수 있다

 

select GETUTCDATE();

 

 

아래는 날짜와 관련된 기능들이다

select * 
from DateTimeTest
where time >= '20100101'

select GETUTCDATE();

select DATEADD(YEAR, 1, '20230101');
select DATEADD(DAY, 1, '20230101');

select GETUTCDATE();
--빼기도 가능하다
select DATEADD(SECOND, -30, '20230101');

--시간 차이
select DATEDIFF( SECOND, '20230103','20230102');

--문자 에서 특정 날자 등을 갖고 오고 싶을때
select DATEPART(DAY, '20230507');

--위의 결과와 동일하다
select DAY('20230507');

 

위 실행 결과 화면

 

반응형
반응형

SELECT CAST('20230124' AS DATETIME)

 

위 구문은 이미지처럼 결과가 나타나게 되고

컬럼 타입은 DATETIME 이다

DATETIME  = 날짜와 시분초가 같이 있는 타입이다

 

 

SELECT CAST('20230124 05:03' AS DATETIME)

 

 

DATE 를 넣는 문자 형태는 다음과 같은 유형들이있다

YYYYMMDD

YYYYMMDD hh:mm:ss.nnn

YYY-MM-DDThh:mm

 

위 예제는 두번째 포맷에 맞춰 넣은 예시이다

 

 

  SELECT GETDATE();
  SELECT CURRENT_TIMESTAMP;

 

 

 

날차 추가하기

USE [BaseballData]
GO

INSERT INTO [dbo].[DateTimeTest]
           ([time])
     VALUES
		--('20090909')
          (CURRENT_TIMESTAMP)
GO






use BaseballData;

SELECT * 
FROM DateTimeTest;

-- 은 주석이다

 

 

 

SELECT * 
FROM DateTimeTest;

몇번 추가한 모습

 

 

조건식으로 비교할때 다음 처럼 할 수 있다 두개의 결과는 같다

use BaseballData;

SELECT * 
FROM DateTimeTest
where time >= CAST('20200101' as DATETIME);


SELECT * 
FROM DateTimeTest
where time >= '20200101';

CAST 로  DATETIME 으로 변홚을 하나 그냥 문자로 넣으나 결과는 같다

 

 

 

UTC TIME 은 어느 나라에서나 표준으로 사용 하는 시간이다

나라마다 시간이 다른데 각 로컬국가의 시간대로 하면 안되고 UTC 로 시간을 계산해야한다

 

SELECT GETUTCDATE();

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형
반응형

 

원래 데이터

나이 구하기

select 2023-birthYear as koreanAge 
from players
where deathYear is null and birthYear is not null
order by koreanAge

 

 

 

쿼리 순서는 

 

from

where

select

orderby 

순 이기 때문에 위에서 쓰여진 네이밍을 해당 명령 포함 하단에서 쓸 수 있다

 

이런 경우엔 에러가 나는것을 알 수 있다

 

 

 

그래서 이렇게 다시 한번 birthYear 를 써서 연산해야한다

select 2023-birthYear as koreanAge 
from players
where deathYear is null and birthYear is not null and (2023-birthYear <= 80 )
order by koreanAge

 

 

 

 

 

 

그 외 일반적인 규칙들

  1. select 3-NULL 의 결과는 NULL 이다
  2. 정수 나누기 정수는 수학적으로 소수로 떨어진다해도 정수고
  3. 분모를 소수로 .0 처리해주면 소수로 나온다
  4. 0 으로 나누면 오류가 난다
  5. ROUND, POWER, COS 함수등들도 제공된다

 

그 외 연산들 참고 

https://learn.microsoft.com/en-us/sql/t-sql/functions/mathematical-functions-transact-sql?view=sql-server-ver16 

 

Mathematical Functions (Transact-SQL) - SQL Server

Mathematical Functions (Transact-SQL)

learn.microsoft.com

 

반응형
반응형
USE BaseballData;

select *
from players
where birthYear is not null
ORDER BY birthYear ASC;

기본이 asc 오름차순이고
desc 는 내림 차순을 의미한다

 

 

아래 코드는 앞선 정렬 순서가 같다면 그다음 순의 정렬 순으로 정렬 되는 것의 반복이다

select *
from players
where birthYear is not null
ORDER BY birthYear DESC, birthMonth desc, birthDay desc;

 

 

is null 같은 null 은 구현이 각각 다를 수 있어서 db를 옮길 일이 있다면 없는 것으로 생각하고 구현하는 것이 좋다

 

 

 

상위 10명을 보여주기 

select TOP(10) *
from players
where birthYear is not null
ORDER BY birthYear DESC, birthMonth desc, birthDay desc;

 

 

전체 데이터 중에서 상위(top) percent 로 해당하는 분포의 데이터를 가져올 수 도 있다

select TOP 0.07 percent *
from players
where birthYear is not null
ORDER BY birthYear DESC, birthMonth desc, birthDay desc;

 

top 은 ms sql 에서 있는데 다른 db 에서는 구현이 좀 다를 수 있다

 

 

 

 

 

offset 은 나온 결과에서 10 개를 건너 뛴 다음의 fetch next 로 3개만 출력 할때 아래처럼 쓸 수 있다

select *
from players
where birthYear is not null
ORDER BY birthYear DESC, birthMonth desc, birthDay desc
OFFSET 10 rows fetch next 3 rows only;

offset 이 구문은 다른 db 에서도 존재하는 구문이다

반응형
반응형
select *
from players
where birthCity LIKE 'New%';

% 는 뒤로 모든 문자가 다 올수 있고

_ 는 한문자만 올 수 있다

 

패턴 매칭을 시작할땐 LIKE 키워드를 넣어준다

 

 

 

 

 

아래 처럼 _ 만 하면 New 다음 한문자가 오는 케이스가 없어서 아무것도 조회가 안되는것을 알 수 있다

반응형
반응형

기본 사칙 연산이 select 에서 가능하다

select 33-30

 

 

 

from 이 select 보다 먼저 실행 된다, 영어 순서를 생각하면 이해하기에 좀더 편하다

SELECT nameFirst as name, nameLast, birthYear, birthCountry
FROM players
WHERE birthYear = 1974 OR birthCountry ='USA'​

 

 

SELECT nameFirst as name, nameLast, birthYear
FROM players
where birthYear != 1866

 

 

 

요소에 null 이 있는 경우는 is null

null 이 없는 경우만 볼려면 is not null 로 조건을 넣어주면 된다

 

select *
from players
where deathYear is not null

 

 

 

select *
from players
where deathYear is null

 

반응형
반응형

 

 

 

https://www.microsoft.com/ko-kr/sql-server/sql-server-downloads

반응형
반응형

2019-2022 Conference/Session Demos and Sample Database

 

 

 

19 버전이 Preview 로 나왔습니다

Download SQL Server Management Studio (SSMS) 19 (Preview)

 Download SQL Server Management Studio (SSMS) 19 (Preview)

https://learn.microsoft.com/en-us/sql/ssms/download-sql-server-management-studio-ssms-19?view=sql-server-ver16

 

 

 

ref : https://www.sqlskills.com/sql-server-resources/sql-server-demos/

반응형
반응형

 

command 패턴으로 서버에서 처리할 명령을 바로 처리하지 않고 우선 처리할 명령을 Queue 에 먾어 넣어 모아두었다가

이 방식은 서버가 처리 할수 있을때 처리 하는 방식으로 직접적으로 명령을 다수의 스레드가 한번에 처리 할때 lock 을 걸어야 하는 상황에서의 부하를 줄일 수 있다


 

설명

이전까지는 클라이언트 세션을 연결되자마자 바로 GameRoom의 List에 넣어주고 있었지만, 이번에는 JobQueue 개념을 이용해 Queue에 일단 GameRoom의 List에 넣어주는 작업을 '예약'해주고 있다.


코드

ServerCore

JobQueue.cs

public interface IJobQueue
    {
        void Push(Action job);
    }

    public class JobQueue : IJobQueue
    {
        Queue<Action> _jobQueue = new Queue<Action>();  
        object _lock = new object();
        bool _flush = false; // 큐에 쌓인걸 '자신이' 실행할건지. 누군가 하고 있으면 자신은 하지 않는다.

        public void Push(Action job)
        {
            bool flush = false;

            lock(_lock)
            {
                _jobQueue.Enqueue(job);
                if (_flush == false)
                    flush = _flush = true;

            }

            if (flush)
                Flush();
        }

        void Flush()
        {
            while(true)
            {
                // 하나씩 꺼내는 와중에도 다른 애가 Push해서 JobQueue에다 넣을 수 있기 때문에 Pop을 할 때 lock을 잡아줘야 한다.
                Action action = Pop();
                if (action == null)
                    return;

                action.Invoke();
            }
        }

        Action Pop()
        {
            lock(_lock)
            {
                if(_jobQueue.Count == 0)
                {
                    _flush = false;
                    return null;
                }
                return _jobQueue.Dequeue(); 
            }
        }
    }
public class Listener
	{
		Socket _listenSocket;
		Func<Session> _sessionFactory;

		public void Init(IPEndPoint endPoint, Func<Session> sessionFactory, int register = 10, int backlog = 10)
		{
			_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
			_sessionFactory += sessionFactory;

			// 문지기 교육
			_listenSocket.Bind(endPoint);

			// 영업 시작
			// backlog : 최대 대기수
			_listenSocket.Listen(backlog);

			for (int i = 0; i < register; i++)
			{
				SocketAsyncEventArgs args = new SocketAsyncEventArgs();
				args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
				RegisterAccept(args);
			}
		}
        
        ...

Server

GameRoom.cs

 class GameRoom : IJobQueue
    {
        List<ClientSession> _sessions = new List<ClientSession>();
        JobQueue _jobQueue = new JobQueue();

        public void Push(Action job)
        {
            _jobQueue.Push(job);
        }

        public void Broadcast(ClientSession session, string chat)
        {
            // 이 부분은 다른 쓰레드와 공유하고 있지 않음
            S_Chat packet = new S_Chat();
            packet.playerId = session.SessionId;
            packet.chat = $"{chat} 나는 {packet.playerId}";
            ArraySegment<byte> segment = packet.Write();

            foreach (ClientSession s in _sessions)
                s.Send(segment);
            
        }

        public void Enter(ClientSession session)
        {
            _sessions.Add(session);
            session.Room = this;
        }

        public void Leave(ClientSession session)
        {
            _sessions.Remove(session);
        }
    }

ClientSession.cs

class ClientSession : PacketSession
	{
		public int SessionId { get; set; }	
		public GameRoom Room { get; set; }

		public override void OnConnected(EndPoint endPoint)
		{
			Console.WriteLine($"OnConnected : {endPoint}");

			Program.Room.Push(() => Program.Room.Enter(this));
		}

		public override void OnRecvPacket(ArraySegment<byte> buffer)
		{
			PacketManager.Instance.OnRecvPacket(this, buffer);
		}

		public override void OnDisconnected(EndPoint endPoint)
		{
			SessionManager.Inst.Remove(this);
			if(Room != null)
            {
				Room.Push(() => Room.Leave(this));
				Room = null;
            }
			Console.WriteLine($"OnDisconnected : {endPoint}");
		}

		public override void OnSend(int numOfBytes)
		{
			Console.WriteLine($"Transferred bytes: {numOfBytes}");
		}
	}

PacketHandler.cs

class PacketHandler
{
	public static void C_ChatHandler(PacketSession session, IPacket packet)
	{
		C_Chat chatPacket = packet as C_Chat;
		ClientSession clientSession = session as ClientSession;

		if (clientSession.Room == null)
			return;

		GameRoom room = clientSession.Room;

		// 할 일을 바로 해주는게 아니라 Push
		room.Push(() => room.Broadcast(clientSession, chatPacket.chat));
	}
}
반응형

'서버(Server) > Server' 카테고리의 다른 글

동기화 처리 : 탈것  (0) 2023.03.18
TCP : 3 way handshake(연결), 4 way handshake(종료)  (0) 2023.03.06
채팅서버  (0) 2023.01.06
Protobuf 로 패킷 보내기  (0) 2023.01.05
PacketSession  (0) 2022.12.29
반응형

설명

Server의 Main에서 Listener를 Init할때 ClientSession 객체를 바로 생성해주는 것이 아닌, SessionManager를 통해 Generate해서 모든 세션을 관리하는 방식을 채택했다.
세션마다 고유 번호를 할당하고 id를 키값으로 Dictionary에 ClientSession을 넣어서 생성, 찾기, 삭제를 할 때마다 lock을 걸어 동시 접근을 차단하는 식으로 구현했다.

서버에는 하나의 GameRoom이 존재하며, 하나의 GameRoom에는 List타입의 _sessions가 존재한다. 그리고 GameRoom에서 어떤 하나의 클라이언트가 메시지를 서버에게 전송하면 서버는 입장 중인 모든 세션 객체에 그 메시지를 Broadcast한다.

하나의 ClientSession이 서버와의 접속이 끊어질 경우, OnDisconnected함수가 호출되어 SessionManager.Remove와 해당 세션이 참여 중인 GameRoom이 있을 경우 GameRoom.Leave() 함수 호출을 통해 퇴장하도록 했다.

10명의 유저가 한 공간에 존재한다고 할때, 한 명의 유저가 메시지를 전송하면, 그 공간 안에 10명의 유저에게 뿌려줘야 한다. 그렇다면 10명의 유저가 동시에 메시지를 하나씩 전송한다면, 10*10=100번의 패킷을 전송해야한다는 말이다. 시간복잡도로 말하자면 O(n^2)가 된다. 그래서 n을 100, 1000으로 늘리면 늘릴수록 서버에 부담이 갈 수 있다.

코드를 그대로 실행해보면 Broadcast 부분에서 수많은 작업자 스레드가 lock에서 대기하고 있는 모습을 볼 수 있다. 하나의 쓰레드가 foreach문을 다 돌 때까지 다른 쓰레드가 대기를 할 수 밖에 없는 상황인데, 최악인 것은 쓰레드가 처리를 못하고 시간을 끌면 작업자 쓰레드를 새로 만들어버리는 악순환이 발생한다는 것이다.

따라서 모든 로직을 lock을 잡아 실행하는 것이 아니라 GameRoom에 Queue를 하나 만들고, 쓰레드들이 일감을 queue에 넣어두고 대기하지 말고 각자 할 일을 하러 가게끔 만들면 된다. 그런 큐를 JobQueue라고 한다.

 

 

 

코드

PDL.xml

<?xml version="1.0" encoding="utf-8" ?>
<PDL>
  <packet name="C_Chat">
	  <string name="chat"/>
  </packet>
  <packet name="S_Chat">
	<int name="playerId"/>
	<string name="chat"/>
  </packet>
</PDL>

Server

GameRoom.cs

 class GameRoom
    {
        List<ClientSession> _sessions = new List<ClientSession>();
        object _lock = new object();

        public void Broadcast(ClientSession session, string chat)
        {
            // 이 부분은 다른 쓰레드와 공유하고 있지 않음
            S_Chat packet = new S_Chat();
            packet.playerId = session.SessionId;
            packet.chat = $"{chat} 나는 {packet.playerId}";
            ArraySegment<byte> segment = packet.Write();

            lock(_lock)
            {
                foreach (ClientSession s in _sessions)
                    s.Send(segment);
            }
        }

        public void Enter(ClientSession session)
        {
            lock (_lock)
            {
                _sessions.Add(session);
                session.Room = this;
            }
        }

        public void Leave(ClientSession session)
        {
            lock (_lock)
            {
                _sessions.Remove(session);
            }
        }
    }

SessionManager.cs

 class SessionManager
    {
        static SessionManager _session = new SessionManager();
        public static SessionManager Inst { get { return _session; } }

        // 세션마다 고유 번호
        int _sessionId = 0;
        Dictionary<int, ClientSession> _sessions = new Dictionary<int, ClientSession> ();
        object _lock = new object ();   

        public ClientSession Generate()
        {
            lock(_lock)
            {
                int sessionId = ++_sessionId;

                ClientSession session = new ClientSession ();
                session.SessionId = sessionId;
                _sessions.Add(sessionId, session);

                Console.WriteLine($"Connected : {sessionId}");

                return session;
            }
        }

        public ClientSession Find(int id)
        {
            lock(_lock)
            {
                ClientSession session = null;
                _sessions.TryGetValue (id, out session);
                return session;
            }
        }

        public void Remove(ClientSession session)
        {
            lock(_lock)
            {
                _sessions.Remove(session.SessionId);
            }
        }
    }

ClientSession.cs

class ClientSession : PacketSession
	{
		public int SessionId { get; set; }	
		public GameRoom Room { get; set; }

		public override void OnConnected(EndPoint endPoint)
		{
			Console.WriteLine($"OnConnected : {endPoint}");

			Program.Room.Enter(this);
		}

		public override void OnRecvPacket(ArraySegment<byte> buffer)
		{
			PacketManager.Instance.OnRecvPacket(this, buffer);
		}

		public override void OnDisconnected(EndPoint endPoint)
		{
			SessionManager.Inst.Remove(this);
			if(Room != null)
            {
				Room.Leave(this);
				Room = null;
            }
			Console.WriteLine($"OnDisconnected : {endPoint}");
		}

		public override void OnSend(int numOfBytes)
		{
			Console.WriteLine($"Transferred bytes: {numOfBytes}");
		}
	}

PacketHandler.cs

class PacketHandler
{
	public static void C_ChatHandler(PacketSession session, IPacket packet)
	{
		C_Chat chatPacket = packet as C_Chat;
		ClientSession clientSession = session as ClientSession;

		if (clientSession.Room == null)
			return;

		clientSession.Room.Broadcast(clientSession, chatPacket.chat);
	}
}

DummyClient

Program.cs

class Program 
	{
		static void Main(string[] args)
		{
			// DNS (Domain Name System)
			string host = Dns.GetHostName();
			IPHostEntry ipHost = Dns.GetHostEntry(host);
			IPAddress ipAddr = ipHost.AddressList[0];
			IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

			Connector connector = new Connector();

			connector.Connect(endPoint, () => { return SessionManager.Inst.Generate(); }, 10); // 클라 숫자 10개 접속

			while (true)
			{
				try
				{
					SessionManager.Inst.SendForEach();
				}
				catch (Exception e)
				{
					Console.WriteLine(e.ToString());
				}

				Thread.Sleep(250);
			}
		}
	}

SessionManager.cs

  class SessionManager
    {
        static SessionManager _session = new SessionManager();
        public static SessionManager Inst { get { return _session; } }

        List<ServerSession> _sessions = new List<ServerSession>();
        object _lock = new object();
        public void SendForEach()
        {
            lock (_lock)
            {
                foreach (ServerSession session in _sessions)
                {
                    C_Chat chatPacket = new C_Chat();
                    chatPacket.chat = $"Hello Server!!!";
                    ArraySegment<byte> segment = chatPacket.Write();

                    session.Send(segment);
                }
            }
        }

        public ServerSession Generate()
        {
            lock(_lock)
            {
                ServerSession session = new ServerSession();
                _sessions.Add(session);
                return session;
            }
        }
    }

PacketHandler.cs

class PacketHandler
{
	public static void S_ChatHandler(PacketSession session, IPacket packet)
	{
		S_Chat chatPacket = packet as S_Chat;
		ServerSession serverSession = session as ServerSession;
		
        Console.WriteLine(chatPacket.chat);
	}
}

 

 

ref : https://velog.io/@fere1032/%EC%B1%84%ED%8C%85%ED%85%8C%EC%8A%A4%ED%8A%B8-1

반응형
반응형

아래는 프로토버프에서 구조화된 구조체를 만들기 위한 프로토버프의 문법으로 만든 예시이다

 

위 처럼 패킷을 프로토 버프 문법으로 작성한다음 

protobuf 에서 제공하는 컴파일로 컴파일 하게 되면 직렬화하여 넣을 수 있는 구조가 만들어지고

다음 처럼 C++ 에서 사용 하여 데이터를 넣을 수 있다

 

add_buffs() 는 데이터를 내무적으로 추가하고 포인터를 얻어와 data 포인터를 통해서 데이터를 쓰는 방식이다

 

 

sendbuffer 에 패킷 데이터를 밀어 넣기

->Buffer());  //위 코드 중 짤린 부분

 

S_TEST 패킷을 만들어 데이터를 넣은 모습이다

 

 

실제 패킷 보내기처리

 

 

 

 

다음은 역질렬화방법이다

헤더 만큼은 건너 뛰고 S_TEST::pkt 에 데이터를 담아온다

 

 

 

 

 

 

 

 

 


 

프로토토콜 버퍼는 구글에서 개발하고 오픈소스로 공개한, 직렬화 데이타 구조 (Serialized Data Structure)이다. C++,C#, Go, Java, Python, Object C, Javascript, Ruby 등 다양한 언어를 지원하며 특히 직렬화 속도가 빠르고 직렬화된 파일의 크기도 작아서 Apache Avro 파일 포맷과 함께 많이 사용된다.

(직렬화란 데이타를 파일로 저장하거나 또는 네트워크로 전송하기 위하여 바이너리 스트림 형태로 저장하는 행위이다.)

 

특히 GRPC 라는 네트워크 프로토콜의 경우 HTTP 2.0 을 기반으로 하면서, 메세지를 이 프로토콜 버퍼를 이용하여 직렬화하기 때문에, 프로토콜 버퍼를 이해해놓으면 GRPC를 습득하는 것이 상대적으로 쉽다.

 

프로토콜 버퍼는 하나의 파일에 최대 64M까지 지원할 수 있으며, 재미있는 기능중 하나는 JSON 파일을 프로토콜 버퍼 파일 포맷으로 전환이 가능하고, 반대로 프로토콜 버퍼 파일도 JSON으로 전환이 가능하다.

설치 및 구성

프로토콜 버퍼 개발툴킷은 크게 두가지 부분이 있다. 데이타 포맷 파일을 컴파일 해주는 protoc 와 각 프로그래밍 언어에서 프로토콜 버퍼를 사용하게 해주는 라이브러리 SDK가 있다.

 

protoc 컴파일러와, 각 프로그래밍 언어별 SDK는 https://github.com/google/protobuf/releases  에서 다운 받으면 된다.

 

protoc 는 C++ 소스 코드를 직접 다운 받아서 컴파일하여 설치할 수 도 있고, 아니면 OS 별로 미리 컴파일된 바이너리를 다운받아서 설치할 수 도 있다.  

 

각 프로그래밍 언어용 프로토콜 버퍼 SDK는 맞는 버전을 다운 받아서 사용하면 된다. 파이썬 버전 설치 방법은  https://github.com/google/protobuf/tree/master/python 를 참고한다.

이 글에서는 파이썬 SDK 버전을 기준으로 설명하도록 한다.

구조 및 사용 방법

프로토콜 버퍼를 사용하기 위해서는 저장하기 위한 데이타형을 proto file 이라는 형태로 정의한다. 프로토콜 버퍼는 하나의 프로그래밍 언어가 아니라 여러 프로그래밍 언어를 지원하기 때문에, 특정 언어에 종속성이 없는 형태로 데이타 타입을 정의하게 되는데, 이 파일을 proto file이라고 한다.

이렇게 정의된 데이타 타입을 프로그래밍 언어에서 사용하려면, 해당 언어에 맞는 형태의 데이타 클래스로 생성을 해야 하는데, protoc 컴파일러로 proto file을 컴파일하면, 각 언어에 맞는 형태의 데이타 클래스 파일을 생성해준다.

 

다음은 생성된 데이타 파일을 프로그래밍 언어에서 불러서, 데이타 클래스를 사용하면 된다.

 

 

 

 

 

 

 

 

 

 

ref : https://bcho.tistory.com/1182

반응형

'서버(Server) > Server' 카테고리의 다른 글

채팅서버 JobQueue 방식으로 부하 줄이기(command 패턴)  (0) 2023.01.06
채팅서버  (0) 2023.01.06
PacketSession  (0) 2022.12.29
TCP, UDP 차이  (0) 2022.12.27
send 시 데이터를 취합해 한번에 보내기  (0) 2022.12.25
반응형

 

결과 화면인데 패킷중 첫번째를 size로 받아와 size 바이트 만큼 받아 왔으면 그다음 id 그리고 데이터를 순차적으로 읽어온다, 여기서 패킷은 TCP 로 전송 될경우 데이터 한 덩어리중 일부가 지연으로 나중에 올경우를 대비해 

 

패킷 중 사이즈가 2바이트라면 2바이트를 먼저 받고 해당 사이즈를 먼저 추출한다음 전체 바이트 만큼 모두 도착 할때까지 수신을 반복한다, 그리고 모두다 데이터를 수신 받았다면 이후부터 패킷의 내용들을 까서 데이터로 받는 처리를 하게 된다

 

TCP 는 데이터의 순서가 보장 됨으로 나중에 왔다 하더라고 모두 전송된 데이터의 비트들은 정상적으로 정렬되어 있다

 

 

public override void OnRecvPacket(ArraySegment<byte> buffer)
		{
			int pos = 0;

			ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset); 
			pos += 2;
			ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + pos);
			pos += 2;

			// TODO
			switch ((PacketID)id)
			{
				case PacketID.PlayerInfoReq:
					{
						long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + pos);
						pos += 8;
					}
					break;
				case PacketID.PlayerInfoOk:
					{
						int hp = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos);
						pos += 4;
						int attack = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos);
						pos += 4;
					}
					//Handle_PlayerInfoOk();
					break;
				default:
					break;
			}

			Console.WriteLine($"RecvPacketId: {id}, Size {size}");
		}

 

 

Thread Local Storage(TLS) 

TLS : 스레드 빌딩 블록으로 스레드 마다 고유한 메모리 영역에 할당되게 된다

public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; });

 

 

 


코드

ServerCore

Session.cs

namespace ServerCore
{
	public abstract class PacketSession : Session
	{
		public static readonly int HeaderSize = 2;

		// size = size 포함한 전체 패킷 크기
		// [size(2)][packetId(2)][ ... ][size(2)][packetId(2)][ ... ]
		public sealed override int OnRecv(ArraySegment<byte> buffer) // 오버라이드 다시 불가
		{
			int processLen = 0;

			while (true)
			{
				// 최소한 헤더는 파싱할 수 있는지 확인
				if (buffer.Count < HeaderSize)
					break;

				// 패킷이 완전체로 도착했는지 확인
				ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset); // ushort
				if (buffer.Count < dataSize)
					break;

				// 여기까지 왔으면 패킷 조립 가능. new 사용했다고 힙에다 할당해주는게 아니라 스택 복사
				OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));
				
				processLen += dataSize;
				// [size(2)][packetId(2)][ ... ] 다음 부분으로 위치 변경
				buffer = new ArraySegment<byte>(buffer.Array, buffer.Offset + dataSize, buffer.Count - dataSize);
			}

			return processLen;
		}

		public abstract void OnRecvPacket(ArraySegment<byte> buffer);
	}

	public abstract class Session
	{
		Socket _socket;
		int _disconnected = 0;

		RecvBuffer _recvBuffer = new RecvBuffer(1024);

		object _lock = new object();
		Queue<ArraySegment<byte>> _sendQueue = new Queue<ArraySegment<byte>>();
		List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
		SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
		SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();

		public abstract void OnConnected(EndPoint endPoint);
		public abstract int  OnRecv(ArraySegment<byte> buffer); // 얼마만큼 데이터를 처리했는지 리턴
		public abstract void OnSend(int numOfBytes);
		public abstract void OnDisconnected(EndPoint endPoint);

		public void Start(Socket socket)
		{
			_socket = socket;

			_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
			_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);

			RegisterRecv();
		}

		public void Send(ArraySegment<byte> sendBuff)
		{
			lock (_lock)
			{
				_sendQueue.Enqueue(sendBuff);
				if (_pendingList.Count == 0)
					RegisterSend();
			}
		}

		public void Disconnect()
		{
			if (Interlocked.Exchange(ref _disconnected, 1) == 1)
				return;

			OnDisconnected(_socket.RemoteEndPoint);
			_socket.Shutdown(SocketShutdown.Both);
			_socket.Close();
		}

		#region 네트워크 통신

		void RegisterSend()
		{
			while (_sendQueue.Count > 0)
			{
				ArraySegment<byte> buff = _sendQueue.Dequeue();
				_pendingList.Add(buff);
			}
			_sendArgs.BufferList = _pendingList;

			bool pending = _socket.SendAsync(_sendArgs);
			if (pending == false)
				OnSendCompleted(null, _sendArgs);
		}

		void OnSendCompleted(object sender, SocketAsyncEventArgs args)
		{
			lock (_lock)
			{
				if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
				{
					try
					{
						_sendArgs.BufferList = null;
						_pendingList.Clear();

						OnSend(_sendArgs.BytesTransferred);

						if (_sendQueue.Count > 0)
							RegisterSend();
					}
					catch (Exception e)
					{
						Console.WriteLine($"OnSendCompleted Failed {e}");
					}
				}
				else
				{
					Disconnect();
				}
			}
		}

		void RegisterRecv()
		{
			_recvBuffer.Clean(); // 커서가 너무 뒤로 가있는 상태 방지
			// 유효한 범위 설정. 다음으로 버퍼를 받을 공간 Set.
			ArraySegment<byte> segment = _recvBuffer.WriteSegment;
			_recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count); // count=freesize, 이만큼 받을 수 있다.

			bool pending = _socket.ReceiveAsync(_recvArgs);
			if (pending == false)
				OnRecvCompleted(null, _recvArgs);
		}

		void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
		{
			if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
			{
				try
				{
					// Write 커서 이동
					if (_recvBuffer.OnWrite(args.BytesTransferred) == false)
					{
						Disconnect();
						return;
					}

					// 컨텐츠 쪽으로 데이터를 넘겨주고 얼마나 처리했는지 받는다
					int processLen = OnRecv(_recvBuffer.ReadSegment);
					if (processLen < 0 || _recvBuffer.DataSize < processLen)
					{
						Disconnect();
						return;
					}

					// Read 커서 이동
					if (_recvBuffer.OnRead(processLen) == false)
					{
						Disconnect();
						return;
					}

					RegisterRecv();
				}
				catch (Exception e)
				{
					Console.WriteLine($"OnRecvCompleted Failed {e}");
				}
			}
			else
			{
				Disconnect();
			}
		}

		#endregion
	}
}

SendBuffer

namespace ServerCore
{
	public class SendBufferHelper
	{
		// 전역이지만 내 쓰레드에서만 사용할 수 있음.
		public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; }); // 처음 만들어질때 null만 리턴하도록

		public static int ChunkSize { get; set; } = 4096 * 100;

		public static ArraySegment<byte> Open(int reserveSize)
		{
			if (CurrentBuffer.Value == null) // SendBuffer 한번도 사용 안한 상태
				CurrentBuffer.Value = new SendBuffer(ChunkSize);

			if (CurrentBuffer.Value.FreeSize < reserveSize)
				CurrentBuffer.Value = new SendBuffer(ChunkSize); // 기존 청크 없앤 후 새롭게 할당

			return CurrentBuffer.Value.Open(reserveSize);
		}

		public static ArraySegment<byte> Close(int usedSize)
		{
			return CurrentBuffer.Value.Close(usedSize);
		}
	}

	public class SendBuffer
	{
		// RecvBuffer처럼 Clean이 없는 이유는 내가 사용이 끝난 부분 이전 부분을 다른 세션에서 Session클래스의 _sendQueue에 넣어논 상태일 수 있기 때문에, 즉 누군가 앞부분을 참조 중인 상태일 수 있기 때문에 재위치 시킬 수 없다.
		// 패킷에 가변인자 들어가면 크기 예측하는게 까다롭기 때문에, 크게 할당 받아놓고 자르는 방식
		// [][][][][][][][][u][] : ChunkSize = 4096 * 100
		byte[] _buffer;
		int _usedSize = 0;

		public int FreeSize { get { return _buffer.Length - _usedSize; } }

		public SendBuffer(int chunkSize)
		{
			_buffer = new byte[chunkSize];
		}

		public ArraySegment<byte> Open(int reserveSize) // 요구할 예약 공간
		{
			if (reserveSize > FreeSize)
				return null;

			// 예약공간이기 때문에 usedSize 이동 x
			return new ArraySegment<byte>(_buffer, _usedSize, reserveSize); // usedSize 위치 포함해서 reserveSize만큼
		}

		public ArraySegment<byte> Close(int usedSize) // 예약 사이즈는 3이라도 실제로 2개 사용되었다면 2만큼 범위 리턴
		{
			ArraySegment<byte> segment = new ArraySegment<byte>(_buffer, _usedSize, usedSize);
			_usedSize += usedSize;
			return segment;
		}
	}
}

Server

ClientSession.cs

namespace Server
{
	class Packet
	{
		public ushort size;
		public ushort packetId;
	}

	class PlayerInfoReq : Packet
	{
		public long playerId;
	}

	class PlayerInfoOk : Packet
	{
		public int hp;
		public int attack;
	}

	public enum PacketID
	{
		PlayerInfoReq = 1,
		PlayerInfoOk = 2,
	}

	class ClientSession : PacketSession
	{
		public override void OnConnected(EndPoint endPoint)
		{
			Console.WriteLine($"OnConnected : {endPoint}");
			Thread.Sleep(5000);
			Disconnect();
		}

		public override void OnRecvPacket(ArraySegment<byte> buffer)
		{
			int pos = 0;

			ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset); 
			pos += 2;
			ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + pos);
			pos += 2;

			// TODO
			switch ((PacketID)id)
			{
				case PacketID.PlayerInfoReq: // required
					{
						long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + pos);
						pos += 8;
					}
					break;
				case PacketID.PlayerInfoOk:
					{
						int hp = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos);
						pos += 4;
						int attack = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos);
						pos += 4;
					}
					//Handle_PlayerInfoOk();
					break;
				default:
					break;
			}

			Console.WriteLine($"RecvPacketId: {id}, Size {size}");
		}

		// TEMP
		public void Handle_PlayerInfoOk(ArraySegment<byte> buffer)
		{

		}
        
        ...
}

DummyClient

ServerSession.cs

namespace DummyClient
{
	class Packet
	{
		public ushort size;
		public ushort packetId;
	}

	class PlayerInfoReq : Packet
	{
		public long playerId;
	}

	class PlayerInfoOk : Packet
	{
		public int hp;
		public int attack;
	}

	public enum PacketID
	{
		PlayerInfoReq = 1,
		PlayerInfoOk = 2,
	}

	class ServerSession : Session
	{
		// unsafe : 포인터 조작. 속도가 빠른 장점
		static unsafe void ToBytes(byte[] array, int offset, ulong value) 
		{
			fixed (byte* ptr = &array[offset])
				*(ulong*)ptr = value;
		}

		static unsafe void ToBytes<T>(byte[] array, int offset, T value) where T : unmanaged
		{
			fixed (byte* ptr = &array[offset])
				*(T*)ptr = value;
		}

		public override void OnConnected(EndPoint endPoint)
		{
			Console.WriteLine($"OnConnected : {endPoint}");
			
			// 패킷의 크기는 아래에서 정해짐
			PlayerInfoReq packet = new PlayerInfoReq() { size = 4, packetId = (ushort)PacketID.PlayerInfoReq, playerId = 1001 };


			// 보낸다
			for (int i = 0; i < 5; i++)
			{
				ArraySegment<byte> s = SendBufferHelper.Open(4096);
				//byte[] size = BitConverter.GetBytes(packet.size);
				//byte[] packetId = BitConverter.GetBytes(packet.packetId);
				//byte[] playerId = BitConverter.GetBytes(packet.playerId);

				ushort size = 0; // int로 만들면 TryWriteBytes의 버전 중 int 버전으로 넘겨줌. ToUInt16
				bool success = true;
			
				// destination(Span<byte>)의 공간보다 value의 크기가 크다면 실패
				// offset+size부터 offset-size만큼의 범위
				size += 2;
				success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + size, s.Count - size), packet.packetId);
				size += 2;
				success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + size, s.Count - size), packet.playerId);
				size += 8;
				success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), size);

				ArraySegment<byte> sendBuff = SendBufferHelper.Close(size); // 실질적으로 보내줄 버퍼.

				if (success)
					Send(sendBuff);
			}
		}
        
        ...
}

 

ref : https://velog.io/@fere1032/SendBuffer-PacketSession

반응형

'서버(Server) > Server' 카테고리의 다른 글

채팅서버  (0) 2023.01.06
Protobuf 로 패킷 보내기  (0) 2023.01.05
TCP, UDP 차이  (0) 2022.12.27
send 시 데이터를 취합해 한번에 보내기  (0) 2022.12.25
멀티스레드를 고려한 SendAsync 보내기  (0) 2022.12.24
반응형

TCP와 UDP 의 차이

중요한 차이점은, TCP는 전송순서가 보장 된다는 것과 받는 측에서 전체 메세지중 일부만 받을 수 있다면 일부만 보내고 나머지 일부는 나중에 보낼 수 있다는 TCP 의 성질이 있다

UDP 는 전송 일부를 보내지 않고 전체다 보내거나 보내지 않거나 하는 특징이 있다

 

릴라이어블 UDP : 

UDP를 사용하는데 TCP와 유사하게 분실이 일어날때 TCP 처럼 처리 하거나 전송순서를 보장해주는 기능이 있는 UDP를 개조한 방식을 말한다

 

UDP 사용 => 캐릭터 위치 : 포지션 위치는 계속 보내주기 때문에 중간에 1,2개 정도 유실 된다 해도 새로운 위치로 바로 갱신되는것이 일반 적임으로

 

MMORPG 는 TCP 를 주료 사용하는데 대체적으로 속도가 엄청 중요한 요소는 아니기 때문

반응에 민감한 FPS 경우에는 UDP를 많이 사용한다

반응형
반응형

_pendingList.Count == 0 이면 보내기를 시작한다는 얘기는 현재 send 를 할때 보낼 데이터중 큐에 있는 것을 모두 뽑아와 한번에 보내는 한번의 send 처리가 이뤄지는 단계라는 것이다

이것으 ArraySegment 를 통해 버퍼의 범위를 지정하여 리스트 혀앹로 담아서

_sendArgs.BufferList = 에 할당해 주면 되고 보낼때는 이 전과 동일하게 SendAsync 를 호출해주면 한번에 보내지게 된다

 

그래서 sendAsync 를 할때 여러 리스트로 구성된 데이터를 한번에 보내게 되고 보내는 처리가 완료되면

OnSendCompleted 가 callback 으로 호출 되게 된다

 

이때 callback 은 별도의 소켓 스레드에 의해서 호출 된 것임으로 sendAsync 를 호출한 후와 OnSendCompleted  이 함수가 호출되기 전 이 사이에서 다른 소켓에 의해 _sendQueue 에 데이터가 채워질수 있기 때문에

 

OnSendCompleted  에서는 _sendQueue 에 데이터가 채워진게 있다면 다시 send 하는 처리 => 여기선 RegisterSend() 를 호출하여 send 하는 처리를 다시 반복시켜준다 : 데이터를 보내야 할 것이 있었던것이기 때문에 보내줘야 함

 

 

 

 

 

 

고려해 봐야 할 점

  • 생각해 봐야 할것은 만약 유저가 엄청 많이 몰려있어서 많은 데이터를 다른 유저간에 서로 보내줘야 할때
    이렇게 요청이 올때마다 보내는 것 보단 일정 패킷을 모아서 다른 유저들에게 한번에 보내주는 것이 더 효과 적일 수도 있다
  • 또는 어떤 유저가 악의적으로 요청(데이터)을 계속 보내서 DDOs 서버에서 이것을 감당하기 힘든 상태가 되는 경우가 발생할 수 있음으로 비정상적으로 많은 데이터를 어떤 클라에서 보내고 있다면 해당 연결을 Disconnect 처리해서 이것을 막아야한다

 

 

 

 

 

반응형

'서버(Server) > Server' 카테고리의 다른 글

PacketSession  (0) 2022.12.29
TCP, UDP 차이  (0) 2022.12.27
멀티스레드를 고려한 SendAsync 보내기  (0) 2022.12.24
_socket.ReceiveAsync 비동기 처리  (0) 2022.12.23
비동기 accept 처리 : _listenSocket.AcceptAsync  (0) 2022.12.23
반응형

send 함수를 멀티스레드로 보내는데 우선 queue 에 담아 놓고

현재 보낼 수 있으면 큐에 있는 내용을 보내고 보낼수 없는 상황이라면 큐에 보낼 내용을 담아 놓기만한다

 

보낼수 있는 상황과 보낼수 없는 상황은

우선 한번 SendAsync 를 할때 동시에 할수는 없고 한번에 하나만 보낼수 있음으로 SendAsync 가 완료 될때까지 다른 스레드가 SendAsync 를 하지 못하게 데이터만 큐에 쌓아 놓게 한뒤 SendAsync 처리가 완료되는 OnSendCompleted 가 호출 되면 SendAsync  처리를 마무리 한다

 

그런데 OnSendCompleted 가 호출 될때 SendAsync 의 한번의 루틴이 끝나기 전에 데이터가 쌓인게 있다면

다시 한번 RegisterSend 를 호출하여 기존에 쌓여 있는것을 다시 send 해주는데

이렇게 하는 이유는 SocketAsyncEventArg 를 여러개 생성하지 않고 재사용하기 위해 이런 처리를 하는건데

이유는 좀금이라도 send 할때의 비용을 아끼기 위해서이다 (유저가 많아질 수록 이 부하도 점점 쌓이기 때문)

 

 

session 코드

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Session
    {
        Socket _socket;
        int _disconnected = 0;

        SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
        Queue<byte[]> _sendQueue = new Queue<byte[]>();
        bool _pending = false;
        object _lock = new object();

        public void Start(Socket socket)
        {
            _socket = socket;

            SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
            recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);


            //receive 할때 데이터를 받는 버퍼를 만들어준다
            recvArgs.SetBuffer(new byte[1024], 0, 1024);        //버퍼를 크게 만들어서 인덱스를 지정하여 분리하여 받아 들일 수도 있다

            //초기에 한번 receive 되도록 등록해 준다
            RegisterRecv(recvArgs);

            _sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);

        }

        
        public void Send(byte[] sendBuff)
        {
            lock(_lock)
            {
                _sendQueue.Enqueue(sendBuff);
                if (_pending == false)
                {
                    RegisterSend();
                }
            }
           
            
        }


        void RegisterSend()
        {
            _pending = true;
            byte[] buff = _sendQueue.Dequeue();
            
            _sendArgs.SetBuffer(buff, 0, buff.Length);
            bool pending = _socket.SendAsync(_sendArgs);
            if (pending == false)
                OnSendCompleted(null, _sendArgs);
            
        }

        //다른 소켓에 의해 OnSendCompleted 호출 될 수 있음으로 lock 을 걸어준다
        private void OnSendCompleted(object value, SocketAsyncEventArgs args)
        {

            lock (_lock)
            {
                if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
                {
                    try
                    {
                        if(_sendQueue.Count > 0)
                        {
                            RegisterSend();     //send를 보내는 처리 과정 사이에 다른 스레드에 의해서 send 가 보내진게 있다면 다시 send 를 보내도록 한다
                        }
                        else
                        {
                            _pending = false;
                        }
                        
                    }
                    catch (System.Exception ex)
                    {
                        Console.WriteLine($"OnSendCompleted Failed {ex}");
                    }
                }
                else
                {
                    DisConnect();
                }
            }

        }

        void RegisterRecv(SocketAsyncEventArgs args)
        {
            bool pending = _socket.ReceiveAsync(args);

            //pending 이  false 인 경우엔 즉 기다리는것이 없이 바로 처리가 될 경우에는
            //OnRecvCompleted를 직접 호출해 줘야 한다, 그 외는 ReceiveAsync 내부에서 나중에 알아서 OnRecvCompleted 를 호출한다
            if (pending == false)       
            {
                OnRecvCompleted(null, args);
            }
        }


        private void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
        {

            //받은 바이트가 0 바이트 이상이고
            if(args.BytesTransferred > 0 &&  args.SocketError == SocketError.Success)
            {
                try
                {
                    //args.Buffer : recvArgs.SetBuffer 에서 설정한 바이트다
                    //args.BytesTransferred : 몇바이트를 받았는지 바이트 수
                    string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
                    Console.WriteLine($"[From client] {recvData}");
                    RegisterRecv(args);
                }
                catch (Exception e)
                {
                    Console.WriteLine($"OnRecevCompleted Failed {e}");
                }
            }
            else
            {
                DisConnect();
            }
        }

        public void DisConnect()
        {
            //멀티 스레드에서 동시에 disconnect를 처리 할 수 있기 때문에 Interlocked를 사용하여 처리한다
            //Exchange는 _disconnected 값을 1로 바꾼다, 그리고 오리지널 값을 반환한다
            if (Interlocked.Exchange(ref _disconnected, 1) == 1)
            {
                //즉 리턴 값이 1 이라는 얘기는 이전에 한번 disconnect 가 됐었다는 얘기 임으로
                //다시 한번 더 disconnect 를 하려고 하면 리턴처리한다 = > 중복 disconnect 방지
                return;
            }
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }
    }
}

 

 

 

서버 시작 부분

using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Text;

namespace ServerCore
{
    class Program
    {
        static Listener _listener = new Listener();

        //클라이언트로부터 접속이 와서 Accept 되었을때 호출 되는 함수
        static void OnAcceptHandler(Socket clientSocket)
        {
            try
            {
                
                Session session = new Session();
                session.Start(clientSocket);        //안에 receive 가 비동기로 처리됨

                //클라이언트로 보내는 처리
                byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to server!");
                session.Send(sendBuff);
                
                Thread.Sleep(1000);
                session.DisConnect();
                //session.DisConnect();     //이렇게 두번 처리 해도 멀티 스레드에 안전하게 처리 했음으로 문제가 되지 않는다

            }
            catch (System.Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        static void Main(string[] args)
        {
            //DNS : Domain Name System
            // 도메인을 하나 등록해서 해당하는 IP 를 찾아오면 관리가 쉬워짐
            //www.google.com => 

            string host = Dns.GetHostName();
            //host = "google.com";            //ipHost.AddressList[0] == {172.217.161.238}
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            //이렇게 GetHostEntry 로 주소를 얻어오는 건 DNS 서버를 통해서 얻어 올 수 있게 됨

            // ipHost.addressList[0] = IPAddress.Parse("        경우에 따라서 ip 주소는 여러개 일 수도 있다 부하 분산을 위해서 addressList
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     //최종 주소

            
            try
            {

                _listener.Init(endPoint, OnAcceptHandler);
                

                while (true)
                {
                }
            }
            catch(Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }
}

 

 

리슨 부분 ( 이전에 있던 Listen 과 유사하다)

using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;

namespace ServerCore
{
    class Listener
    {
        Socket _listenSocket;

        Action<Socket> _onAcceptHandler;
        
        public void Init(IPEndPoint iPEndPoint, Action<Socket> onAcceptHandler)
        {
            //AddressFamily ip version 4,6 에 대한 것 , 위에서 자동으로 만들어줌, 
            //tcp 로 할 경우 stream, tcp 로 설정해준다
            //리슨 하는 자체가 소켓을 하나 만들어야 한다
            _listenSocket = new Socket(iPEndPoint.AddressFamily , SocketType.Stream, ProtocolType.Tcp);
            _onAcceptHandler += onAcceptHandler;

            _listenSocket.Bind(iPEndPoint);        //소켓에 ip 와 포트 할당


            //최대 동시 대기 수, 동시에 들어올대 10명까지만 처리 가능하고 그 위로는 실패가 된다
            //Listen() 메서드는 동시에 여러 클라이언트가 접속되었을 때 큐에 몇 개의 클라이언트가
            //대기할 수 있는지 지정할 수 있는데, 위의 경우는 예시를 위해 10을 넣었다.
            _listenSocket.Listen(10);

            //이건 한번 사용하고 재사용이 가능하다
            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);

            //최초 한번은 등록해준다
            RegisterAccept(args);
            
        }

        void RegisterAccept(SocketAsyncEventArgs args)
        {
            //재사용이 됨으로 null 로 처리한다
            args.AcceptSocket = null;

            //SocketAsyncEventArgs 가 하나일때
            //AcceptAsync 는 대기 하고 있는 클라 접속중 하나에 대해서만 receive 처리를 하고 동시에 두개를 하진 않는다,
            //하나하고 그다음 하나 step by step
            bool pending = _listenSocket.AcceptAsync(args);     //비동기 임으로 예약만 하고 넘어간다, accdept 완료는 eventHandler 를 통해서 완료된다
            if (pending == false)   //false 면 pending 없이 바로 완료 됐다는 얘기임
                OnAcceptCompleted(null, args);
        }

        void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
        {
            if(args.SocketError == SocketError.Success)
            {
                //accept 되어 새로 생성된 소켓을 
                _onAcceptHandler.Invoke(args.AcceptSocket); //넘겨준다
            }
            else
            {
                Console.WriteLine(args.SocketError.ToString());
            }

            //위에 까지 처리가 된것은 accept 가 완료 된것임으로 새로운 accept 를 위해서
            //RegisterAccept 를 다시 호출하여 OnAcceptCompleted 이벤트를 받아 들을 수 있는 상태로 만든다 
            RegisterAccept(args);
        }

        public Socket Accept()
        {

            //return _listenSocket.Accept();    
            //클라의 접속이 있다면 받아오는 처리, 접속이 있을때까지 계속 대기, 즉 다음으로 넘어가지 않는다
            //클라로부터 접속이 왔다면 accept 되어 클라와 별도 통실한 socket 이 생성되어 리턴된다
            //return _listenSocket.Accept();    

            //async 는 비동기로 처리 된다
            //return _listenSocket.AcceptAsync()
            return null;
        }
    }
}

 

 

 

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace DummyClient
{
    class Program
    {
        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     
            
            while (true)
            {
                try
                {
                    Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

                    //소켓으로 서버에 연결한다 , 서버 입장에선 accept 가 된다
                    socket.Connect(endPoint);
                    Console.WriteLine($"connected to {socket.RemoteEndPoint.ToString()}");


                    //서버로 보낸다
                    for(int i=0;i< 5; ++i)
                    {
                        byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello world! {i}  ");
                        int bytesSent = socket.Send(sendBuff);
                    }

                    //서버에서 받는다
                    byte[] recvBuff = new byte[1024];
                    int recvBytes = socket.Receive(recvBuff);
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                    Console.WriteLine($"received : {recvData}");

                    //서버와 통신을 위해 생성했던 소켓 종료 처리
                    socket.Shutdown(SocketShutdown.Both);
                    socket.Close();

                }
                catch (System.Exception ex)
                {
                    Console.WriteLine(ex);
                }
                Thread.Sleep(100);
            }
           

        }
    }
}

클라이언트 더미 코드 또한 동일하다 : 좀 더 다양하게 서버로 접속하게 처리해야 하지만 우선 send 에 만 비동기이면서 멀티스레드로 보낼수 있는 구조로 만드는 것에 초점을 맞춘다

 

반응형
반응형

 

서버코어 부분인데 이 부분에서 listen과 receive 를 하기 위한 초기호및 보내기 데이터 설정 등을 한다

using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Text;

namespace ServerCore
{


    class Program
    {

        

        static Listener _listener = new Listener();


        //클라이언트로부터 접속이 와서 Accept 되었을때 호출 되는 함수
        static void OnAcceptHandler(Socket clientSocket)
        {
            try
            {
                
                Session session = new Session();
                session.Start(clientSocket);        //안에 receive 가 비동기로 처리됨

                //클라이언트로 보내는 처리
                byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to server!");
                session.Send(sendBuff);
                
                Thread.Sleep(1000);
                session.DisConnect();
                //session.DisConnect();     //이렇게 두번 처리 해도 멀티 스레드에 안전하게 처리 했음으로 문제가 되지 않는다

            }
            catch (System.Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        static void Main(string[] args)
        {
            //DNS : Domain Name System
            // 도메인을 하나 등록해서 해당하는 IP 를 찾아오면 관리가 쉬워짐
            //www.google.com => 

            string host = Dns.GetHostName();
            //host = "google.com";            //ipHost.AddressList[0] == {172.217.161.238}
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            //이렇게 GetHostEntry 로 주소를 얻어오는 건 DNS 서버를 통해서 얻어 올 수 있게 됨

            // ipHost.addressList[0] = IPAddress.Parse("        경우에 따라서 ip 주소는 여러개 일 수도 있다 부하 분산을 위해서 addressList
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     //최종 주소

            
            try
            {

                _listener.Init(endPoint, OnAcceptHandler);
                

                while (true)
                {
                }
            }
            catch(Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }
}

 

 

 

리슨을 담당하는 코드

 

Listen() 메서드는 동시에 여러 클라이언트가 접속되었을 때 큐에 몇 개의 클라이언트가 대기할 수 있는지 지정할 수 있는데, 위의 경우는 예시를 위해 10을 넣었다.

using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;

namespace ServerCore
{
    class Listener
    {
        Socket _listenSocket;

        Action<Socket> _onAcceptHandler;
        
        public void Init(IPEndPoint iPEndPoint, Action<Socket> onAcceptHandler)
        {
            //AddressFamily ip version 4,6 에 대한 것 , 위에서 자동으로 만들어줌, 
            //tcp 로 할 경우 stream, tcp 로 설정해준다
            //리슨 하는 자체가 소켓을 하나 만들어야 한다
            _listenSocket = new Socket(iPEndPoint.AddressFamily , SocketType.Stream, ProtocolType.Tcp);
            _onAcceptHandler += onAcceptHandler;

            _listenSocket.Bind(iPEndPoint);        //소켓에 ip 와 포트 할당


            //최대 동시 대기 수, 동시에 들어올대 10명까지만 처리 가능하고 그 위로는 실패가 된다
            _listenSocket.Listen(10);

            //이건 한번 사용하고 재사용이 가능하다
            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);

            //최초 한번은 등록해준다
            RegisterAccept(args);
            
        }

        void RegisterAccept(SocketAsyncEventArgs args)
        {
            //재사용이 됨으로 null 로 처리한다
            args.AcceptSocket = null;

            bool pending = _listenSocket.AcceptAsync(args);     //비동기 임으로 예약만 하고 넘어간다, accdept 완료는 eventHandler 를 통해서 완료된다
            if (pending == false)   //false 면 pending 없이 바로 완료 됐다는 얘기임
                OnAcceptCompleted(null, args);
        }

        void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
        {
            if(args.SocketError == SocketError.Success)
            {
                //accept 되어 새로 생성된 소켓을 
                _onAcceptHandler.Invoke(args.AcceptSocket); //넘겨준다
            }
            else
            {
                Console.WriteLine(args.SocketError.ToString());
            }

            //위에 까지 처리가 된것은 accept 가 완료 된것임으로 새로운 accept 를 위해서
            //RegisterAccept 를 다시 호출하여 OnAcceptCompleted 이벤트를 받아 들을 수 있는 상태로 만든다 
            RegisterAccept(args);
        }

        public Socket Accept()
        {

            //return _listenSocket.Accept();    
            //클라의 접속이 있다면 받아오는 처리, 접속이 있을때까지 계속 대기, 즉 다음으로 넘어가지 않는다
            //클라로부터 접속이 왔다면 accept 되어 클라와 별도 통실한 socket 이 생성되어 리턴된다
            //return _listenSocket.Accept();    

            //async 는 비동기로 처리 된다
            //return _listenSocket.AcceptAsync()
            return null;
        }
    }
}

 

 

 

세션코드로 클라로부터 들어온 데이터를 비동기로 받는 로직이다

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Session
    {
        Socket _socket;
        int _disconnected = 0;
        public void Start(Socket socket)
        {
            _socket = socket;

            SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
            recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);


            //receive 할때 데이터를 받는 버퍼를 만들어준다
            recvArgs.SetBuffer(new byte[1024], 0, 1024);        //버퍼를 크게 만들어서 인덱스를 지정하여 분리하여 받아 들일 수도 있다

            //초기에 한번 receive 되도록 등록해 준다
            RegisterRecv(recvArgs);

        }

        public void Send(byte[] sendBuff)
        {
            _socket.Send(sendBuff);
        }

        void RegisterRecv(SocketAsyncEventArgs args)
        {
            bool pending = _socket.ReceiveAsync(args);

            //pending 이  false 인 경우엔 즉 기다리는것이 없이 바로 처리가 될 경우에는
            //OnRecvCompleted를 직접 호출해 줘야 한다, 그 외는 ReceiveAsync 내부에서 나중에 알아서 OnRecvCompleted 를 호출한다
            if (pending == false)       
            {
                OnRecvCompleted(null, args);
            }
        }

        private void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
        {
            //받은 바이트가 0 바이트 이상이고
            if(args.BytesTransferred > 0 &&  args.SocketError == SocketError.Success)
            {
                try
                {
                    //args.Buffer : recvArgs.SetBuffer 에서 설정한 바이트다
                    //args.BytesTransferred : 몇바이트를 받았는지 바이트 수
                    string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
                    Console.WriteLine($"[From client] {recvData}");
                    RegisterRecv(args);
                }
                catch (Exception e)
                {
                    Console.WriteLine($"OnRecevCompleted Failed {e}");
                }
            }
            else
            {
                
            }
        }

        public void DisConnect()
        {
            //멀티 스레드에서 동시에 disconnect를 처리 할 수 있기 때문에 Interlocked를 사용하여 처리한다
            //Exchange는 _disconnected 값을 1로 바꾼다, 그리고 오리지널 값을 반환한다
            if (Interlocked.Exchange(ref _disconnected, 1) == 1)
            {
                //즉 리턴 값이 1 이라는 얘기는 이전에 한번 disconnect 가 됐었다는 얘기 임으로
                //다시 한번 더 disconnect 를 하려고 하면 리턴처리한다 = > 중복 disconnect 방지
                return;
            }
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }
    }
}

 

 

 

 

 

서버와 클라간의 테스트를 위한 더미 클라이언트 코드

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace DummyClient
{
    class Program
    {
        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     
            
            while (true)
            {
                try
                {
                    Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

                    //소켓으로 서버에 연결한다 , 서버 입장에선 accept 가 된다
                    socket.Connect(endPoint);
                    Console.WriteLine($"connected to {socket.RemoteEndPoint.ToString()}");


                    //서버로 보낸다
                    for(int i=0;i< 5; ++i)
                    {
                        byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello world! {i}  ");
                        int bytesSent = socket.Send(sendBuff);
                    }

                    //서버에서 받는다
                    byte[] recvBuff = new byte[1024];
                    int recvBytes = socket.Receive(recvBuff);
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                    Console.WriteLine($"received : {recvData}");

                    //서버와 통신을 위해 생성했던 소켓 종료 처리
                    socket.Shutdown(SocketShutdown.Both);
                    socket.Close();

                }
                catch (System.Exception ex)
                {
                    Console.WriteLine(ex);
                }
                Thread.Sleep(100);
            }
           

        }
    }
}

 

 

bool pending = _listenSocket.AcceptAsync(args);

SocketAsyncEventArgs 가 하나일때(즉 n 번 선언해 놓고 Register 를 n 번 한게 아닌 n = 1 일때)
AcceptAsync 는 대기 하고 있는 클라 접속중 하나에 대해서만 receive 처리를 하고 동시에 두개를 하진 않는다,
하나하고 그다음 하나 step by step

 

 

클라 접속이 이뤄지더라도 AcceptAsync를 명시적으로 호출해줘야 OnAcceptCompleted가 실행이 된다
즉, AcceptAsync는 일종의 입장 허락의 개념인데 
그 전에 접속을 희망한 클라들은 대기열(큐)에 입장 대기를 하고 있게 되고
OnAcceptCompleted를 호출해서 입장 관련 처리를 다 끝낸 다음,
마지막에 RegisterAccept를 다시 호출해서 다음 입장을 받아주기 때문에
동시에 여러 쓰레드가 OnAcceptCompleted를 실행할 수 없기도 하다



 

Accepting connections asynchronously gives you the ability to send and receive data within a separate execution thread. Before calling the AcceptAsync method, you must call the Listen method to listen for and queue incoming connection requests.

 

 

 

결과 화면 : 더미 클라에서 send 를 연달아 다섯번정도하기 때문에 서버에서 비동기 receive 로 5번이 처리 되는것 것을 볼 수 있다, 콘솔 창에서 보면 알 수 있듯이 receive 가 동시에 멀티 스레드 처럼 5번 처리 되는것이 아니고 한번에 한번만 처리 되는 것이라 볼 수 있는데 현재 Recv 코드는 오직 1번에 1개의 쓰레드만 접근 할 수 있다

 

다시 말해 최초에 RegisterRecv 를 1개만 걸어 놨기 때문에 Register->Completed  .. 과정도 오로지 한번에 1개만 일어난다

 

 

 

ref : https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.acceptasync?view=netframework-4.7.2&f1url=%3FappId%3DDev16IDEF1%26l%3DEN-US%26k%3Dk(System.Net.Sockets.Socket.AcceptAsync)%3Bk(TargetFrameworkMoniker-.NETFramework%2CVersion%253Dv4.7.2)%3Bk(DevLang-csharp)%26rd%3Dtrue 

 

Socket.AcceptAsync Method (System.Net.Sockets)

Accepts an incoming connection.

learn.microsoft.com

 

반응형
반응형

서버 코드

using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Text;

namespace ServerCore
{


    class Program
    {

        

        static Listener _listener = new Listener();


        static void OnAcceptHandler(Socket clientSocket)
        {
            try
            {
                //클라이언트로부터 받아오는 처리
                byte[] recvBuff = new byte[1024];
                int recvBytpes = clientSocket.Receive(recvBuff);
                string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytpes);
                Console.WriteLine($"[From Client] {recvData}");


                //클라이언트로 보내는 처리
                byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to server!");
                clientSocket.Send(sendBuff);

                clientSocket.Shutdown(SocketShutdown.Both);     //연결된 소켓 끊기, 듣기와 말하기를 하지 않겠다는 것
                clientSocket.Close();   //클라와 서버간의 연결 끊기
            }
            catch (System.Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        static void Main(string[] args)
        {
            //DNS : Domain Name System
            // 도메인을 하나 등록해서 해당하는 IP 를 찾아오면 관리가 쉬워짐
            //www.google.com => 

            string host = Dns.GetHostName();
            //host = "google.com";            //ipHost.AddressList[0] == {172.217.161.238}
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            //이렇게 GetHostEntry 로 주소를 얻어오는 건 DNS 서버를 통해서 얻어 올 수 있게 됨

            // ipHost.addressList[0] = IPAddress.Parse("        경우에 따라서 ip 주소는 여러개 일 수도 있다 부하 분산을 위해서 addressList
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     //최종 주소

            
            try
            {

                _listener.Init(endPoint, OnAcceptHandler);
                

                while (true)
                {
                }
            }
            catch(Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }
}

 

 

리스터 코드 : 비동기 accept

using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;

namespace ServerCore
{
    class Listener
    {
        Socket _listenSocket;

        Action<Socket> _onAcceptHandler;
        
        public void Init(IPEndPoint iPEndPoint, Action<Socket> onAcceptHandler)
        {
            //AddressFamily ip version 4,6 에 대한 것 , 위에서 자동으로 만들어줌, 
            //tcp 로 할 경우 stream, tcp 로 설정해준다
            //리슨 하는 자체가 소켓을 하나 만들어야 한다
            _listenSocket = new Socket(iPEndPoint.AddressFamily , SocketType.Stream, ProtocolType.Tcp);
            _onAcceptHandler += onAcceptHandler;

            _listenSocket.Bind(iPEndPoint);        //소켓에 ip 와 포트 할당


            //최대 동시 대기 수, 동시에 들어올대 10명까지만 처리 가능하고 그 위로는 실패가 된다
            _listenSocket.Listen(10);

            //이건 한번 사용하고 재사용이 가능하다
            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);

            //최초 한번은 등록해준다
            RegisterAccept(args);
            
        }

        void RegisterAccept(SocketAsyncEventArgs args)
        {
            //재사용이 됨으로 null 로 처리한다
            args.AcceptSocket = null;

            bool pending = _listenSocket.AcceptAsync(args);     //비동기 임으로 예약만 하고 넘어간다, accdept 완료는 eventHandler 를 통해서 완료된다
            if (pending == false)   //false 면 pending 없이 바로 완료 됐다는 얘기임
                OnAcceptCompleted(null, args);
        }

        void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
        {
            if(args.SocketError == SocketError.Success)
            {
                //accept 되어 새로 생성된 소켓을 
                _onAcceptHandler.Invoke(args.AcceptSocket); //넘겨준다
            }
            else
            {
                Console.WriteLine(args.SocketError.ToString());
            }

            //위에 까지 처리가 된것은 accept 가 완료 된것임으로 새로운 accept 를 위해서
            //RegisterAccept 를 다시 호출하여 OnAcceptCompleted 이벤트를 받아 들을 수 있는 상태로 만든다 
            RegisterAccept(args);
        }

        public Socket Accept()
        {

            //return _listenSocket.Accept();    
            //클라의 접속이 있다면 받아오는 처리, 접속이 있을때까지 계속 대기, 즉 다음으로 넘어가지 않는다
            //클라로부터 접속이 왔다면 accept 되어 클라와 별도 통실한 socket 이 생성되어 리턴된다
            //return _listenSocket.Accept();    

            //async 는 비동기로 처리 된다
            //return _listenSocket.AcceptAsync()
            return null;
        }
    }
}

 

 

더미 클라이언트 코드

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace DummyClient
{
    class Program
    {
        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     
            
            while (true)
            {
                try
                {
                    Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

                    //소켓으로 서버에 연결한다 , 서버 입장에선 accept 가 된다
                    socket.Connect(endPoint);
                    Console.WriteLine($"connected to {socket.RemoteEndPoint.ToString()}");


                    //서버로 보낸다
                    byte[] sendBuff = Encoding.UTF8.GetBytes("Hello world!");
                    int bytesSent = socket.Send(sendBuff);

                    //서버에서 받는다
                    byte[] recvBuff = new byte[1024];
                    int recvBytes = socket.Receive(recvBuff);
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                    Console.WriteLine($"received : {recvData}");

                    //서버와 통신을 위해 생성했던 소켓 종료 처리
                    socket.Shutdown(SocketShutdown.Both);
                    socket.Close();

                }
                catch (System.Exception ex)
                {
                    Console.WriteLine(ex);
                }
                Thread.Sleep(100);
            }
           

        }
    }
}

 

 

AcceptAsync : 비동기 방식으로 클라의 접속을 말한다

AcceptAsync 로 처리하면 서버에서 클라의 접속을 받아 Accept() 함수 처럼 accept 하게 될때 해당 라인에서 무한 대기 하지 않고 클라와 통신하기 위한 전용 socket 을 만들어 리턴하고 서버에선 바로 다음 로직을 처리 할 수 있게 된다

 

bool pending = _listenSocket.AcceptAsync(args);

 

 

결과화면

반응형

'서버(Server) > Server' 카테고리의 다른 글

멀티스레드를 고려한 SendAsync 보내기  (0) 2022.12.24
_socket.ReceiveAsync 비동기 처리  (0) 2022.12.23
기본적인 소켓 프로그래밍  (0) 2022.12.23
소켓 통신  (0) 2022.12.22
통신 모델 OSI 7 계층  (0) 2022.12.22
반응형

 

C# 으로 서버와 클라이언트 기본적인 연결 및 string 데이터 전송 코드이다

 

서버 코드

using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Text;

namespace ServerCore
{


    class Program
    {
        static void Main(string[] args)
        {
            //DNS : Domain Name System
            // 도메인을 하나 등록해서 해당하는 IP 를 찾아오면 관리가 쉬워짐
            //www.google.com => 

            string host = Dns.GetHostName();
            //host = "google.com";            //ipHost.AddressList[0] == {172.217.161.238}
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            //이렇게 GetHostEntry 로 주소를 얻어오는 건 DNS 서버를 통해서 얻어 올 수 있게 됨

            // ipHost.addressList[0] = IPAddress.Parse("        경우에 따라서 ip 주소는 여러개 일 수도 있다 부하 분산을 위해서 addressList
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     //최종 주소

            //AddressFamily ip version 4,6 에 대한 것 , 위에서 자동으로 만들어줌, 
            //tcp 로 할 경우 stream, tcp 로 설정해준다
            //리슨 하는 자체가 소켓을 하나 만들어야 한다
            Socket listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

            try
            {
                listenSocket.Bind(endPoint);        //소켓에 ip 와 포트 할당


                //최대 동시 대기 수, 동시에 들어올대 10명까지만 처리 가능하고 그 위로는 실패가 된다
                listenSocket.Listen(10);


                while (true)
                {
                    Console.WriteLine("Listening .... ");

                    //클라의 접속이 있다면 받아오는 처리, 접속이 있을때까지 계속 대기, 즉 다음으로 넘어가지 않는다
                    //클라로부터 접속이 왔다면 accept 되어 클라와 별도 통실한 socket 이 생성되어 리턴된다
                    Socket clientSocket = listenSocket.Accept();


                    //클라이언트로부터 받아오는 처리
                    byte[] recvBuff = new byte[1024];
                    int recvBytpes = clientSocket.Receive(recvBuff);
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytpes);
                    Console.WriteLine($"[From Client] {recvData}");


                    //클라이언트로 보내는 처리
                    byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to server!");
                    clientSocket.Send(sendBuff);

                    clientSocket.Shutdown(SocketShutdown.Both);     //연결된 소켓 끊기, 듣기와 말하기를 하지 않겠다는 것
                    clientSocket.Close();   //클라와 서버간의 연결 끊기

                }
            }
            catch(Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }
}

 

 

 

클라이언트 코드

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace DummyClient
{
    class Program
    {
        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     
            
            Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

            //소켓으로 서버에 연결한다 , 서버 입장에선 accept 가 된다
            socket.Connect(endPoint);
            Console.WriteLine($"connected to {socket.RemoteEndPoint.ToString()}");


            //서버로 보낸다
            byte[] sendBuff = Encoding.UTF8.GetBytes("Hello world!");
            int bytesSent = socket.Send(sendBuff);

            //서버에서 받는다
            byte[] recvBuff = new byte[1024];
            int recvBytes = socket.Receive(recvBuff);
            string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
            Console.WriteLine($"received : {recvData}");

            //서버와 통신을 위해 생성했던 소켓 종료 처리
            socket.Shutdown(SocketShutdown.Both);
            socket.Close();
            

        }
    }
}

 

결과 창

 

서버에서는 클라에서 보낸 데이터를 보여주고

클라에서는 서버로부터 받은 데이터를 뿌려주고 종료한다

 

반응형

'서버(Server) > Server' 카테고리의 다른 글

_socket.ReceiveAsync 비동기 처리  (0) 2022.12.23
비동기 accept 처리 : _listenSocket.AcceptAsync  (0) 2022.12.23
소켓 통신  (0) 2022.12.22
통신 모델 OSI 7 계층  (0) 2022.12.22
javascript : Class  (0) 2018.08.19

+ Recent posts