이 글을 보고 작성하였다.
지금까지 작성한 소켓통신코드들은 전부 1서버 1클라이언트 구조였다. 다만 현실에서 큰 서버를 운영하려면, 1서버가 n개의 클라이언트를 담당해야한다.
이 때 현명한 누군가는 '예전에 짠 코드를 전부 쓰레드화 시키고 클라이언트 연결할 때 마다 쓰레드를 생성하면 되는거 아니냐!' 라고 말할것이다. 물론 가능하다. 하지만 그렇게 구현하게 되면 몇 가지 단점이 있다.
따라서 쓰레드 없이 다중클라이언트 다루는 코드를 알아보겠다.
리눅스에서 다중클라이언트를 다루는 더 좋은 방법은 select()이다.
셀렉트 커맨드는 다중 file descriptor를 모니터링 할 수 있게 해준다. fd가 active 될 때 까지 대기한다.
select는 interrupt handler로 작동하는데, fd가 데이터를 보내자마다 바로 작동한다.
fd_set은 select를 위한 자료구조이다. fd_set의 함수는 다음과 같다.
fd_set readfds;
// Clear an fd_set
FD_ZERO(&readfds);
// Add a descriptor to an fd_set
FD_SET(master_sock, &readfds);
// Remove a descriptor from an fd_set
FD_CLR(master_sock, &readfds);
//If something happened on the master socket , then its an incoming connection
FD_ISSET(master_sock, &readfds);
select는 다음과 같은 구조이다.
activity = select( max_fd + 1 , &readfds , NULL , NULL , NULL);
select함수의 원형은 다음과 같다.
int select (int n, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct imeval* timeout);
-readfds = 읽기 가능한지 확인
-writefds = 쓰기 가능한지 확인
-exceptfds = 예외가 발생했거나 대역을 넘어서는 데이터가 존재하는지 감시
만약 NULL을 준다면 감시를 시행하지 않는다는뜻이다. 따라서 우리가 작성한 activity는 readfds만 감시하고 나머지는 신경쓰지 않는다.
#include <stdio.h>
#include <string.h> //strlen
#include <stdlib.h>
#include <errno.h>
#include <unistd.h> //close
#include <arpa/inet.h> //close
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/time.h> //FD_SET, FD_ISSET, FD_ZERO macros
#define True 1
#define False 0
#define PORT 10000
int main(int argc, char *argv[]) {
int opt = True;
int master_socket , addrlen , new_socket , client_socket[30], max_clients = 30 , activity, i , valread , sd;
int max_sd;
struct sockaddr_in address;
char buffer[1024];
fd_set readfds;
char *message = (char*)"Echo Daemon v1.0 \r\n";
for (i = 0; i < max_clients; i++) {
client_socket[i] = 0;
}
if ((master_socket = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
if( setsockopt(master_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt)) < 0 ) {
perror("setsocketopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons (PORT);
if (bind(master_socket, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
printf("Listener on port %d \n", PORT);
if (listen(master_socket, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
addrlen = sizeof(address);
puts("waiting for connections...");
while(True) {
FD_ZERO(&readfds);
FD_SET(master_socket, &readfds);
max_sd = master_socket;
for (i = 0; i < max_clients; i++) {
sd = client_socket[i];
if (sd > 0) FD_SET(sd, &readfds);
if (sd > max_sd) max_sd = sd;
}
activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
if ((activity < 0) && (errno != EINTR)) {
printf({"select Error"});
}
if (FD_ISSET(master_socket, &readfds)) {
if ((new_socket = accept(master_socket, (struct sockaddr*)& address, (socklen_t*)& addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
printf("New connection , socket fd is %d , ip is : %s , port : %d\n"
, new_socket , inet_ntoa(address.sin_addr) , ntohs (address.sin_port));
if (send (new_socket, message, strlen(message), 0) != strlen(message)) {
perror("send");
}
puts("Welcome message sent sucessfully");
for (i = 0; i < max_clients; i++) {
if( client_socket[i] = 0) {
client_socket[i] = new_socket;
printf("Adding to list of socket as %d\n", i);
break;
}
}
}
for (i = 0; i < max_clients; i++) {
sd = client_socket[i];
if(FD_ISSET(sd, &readfds)) {
if (valread = read(sd, buffer, 1024) == 0) {
getpeername(sd , (struct sockaddr*)&address , \
(socklen_t*)&addrlen);
printf("Host disconnected , ip %s , port %d \n" ,
inet_ntoa(address.sin_addr) , ntohs(address.sin_port));
close( sd );
client_socket[i] = 0;
} else {
buffer[valread] = '\0';
send(sd, buffer, strlen(buffer), 0);
}
}
}
}
return 0;
}
ssh로 서버실행후 linux 클라이언트, windows 클라이언트 두개를 동시에 연결시도 해보면 다음과 같이 나온다.
리눅스 클라이언트는 귀찮아서 안찍었다
검은화면이 server 아래 vs 화면이 window쪽 클라이언트이다.
보이는것처럼 서로 연결되고 메세지를 주고받는것을 알 수 있다.
다음글에서는 코드를 분석해보겠다.
꽤 길어서 오래걸릴듯하다.