동시성 마스터에 대한 이전 Ruby Magic 기사에서 우리는 Ruby 개발자로서 사용할 수 있는 동시성을 달성하는 세 가지 방법을 소개했습니다. 이 기사는 각 방법에 대해 자세히 살펴보는 3부작 시리즈의 첫 번째 기사입니다.
첫 번째:다중 프로세스 . 이 방법을 사용하면 마스터 프로세스가 여러 작업자 프로세스로 분기됩니다. 작업자 프로세스가 실제 작업을 수행하고 마스터가 작업자를 관리합니다.
<블록 인용>이 문서의 예제에 사용된 전체 소스 코드는 GitHub에서 사용할 수 있으므로 직접 실험해 볼 수 있습니다.
채팅 시스템을 구축하자!
채팅 시스템을 구축하는 것은 동시성에 뛰어드는 좋은 방법입니다. 여러 클라이언트와의 연결을 유지할 수 있는 채팅 시스템의 서버 구성 요소가 필요합니다. 이렇게 하면 한 클라이언트에서 받은 메시지를 연결된 다른 모든 클라이언트에 배포할 수 있습니다.
채팅 서버는 왼쪽 탭에서 실행 중입니다. 오른쪽 탭에서 실행 중인 두 개의 채팅 클라이언트가 있습니다. 클라이언트가 보낸 모든 메시지는 다른 모든 클라이언트가 수신합니다.
채팅 클라이언트
이 기사는 채팅 서버에 초점을 맞추고 있지만, 이 서버와 통신하려면 먼저 채팅 클라이언트가 필요합니다. 다음 코드는 매우 간단한 클라이언트가 될 것입니다. (더 완전한 예제는 GitHub에서 찾을 수 있습니다.)
# client.rb
# $ ruby client.rb
require 'socket'
client = TCPSocket.open(ARGV[0], 2000)
Thread.new do
while line = client.gets
puts line.chop
end
end
while input = STDIN.gets.chomp
client.puts input
end
클라이언트는 포트 2000에서 실행되는 서버에 대한 TCP 연결을 엽니다. 연결되면 puts
서버가 보내는 모든 것이므로 채팅은 터미널 출력에서 볼 수 있습니다. 마지막으로, 입력한 줄을 서버로 보내는 while 루프가 있습니다. 이 루프는 연결된 다른 모든 클라이언트로 보냅니다.
채팅 서버
이 예에서 클라이언트는 다른 클라이언트와 통신하기 위해 채팅 서버에 연결합니다. 세 가지 동시성 접근 방식 모두에 대해 Ruby 표준 라이브러리의 동일한 TCP 서버를 사용합니다.
# server_processes.rb
# $ ruby server_processes.rb
require 'socket'
puts 'Starting server on port 2000'
server = TCPServer.open(2000)
이 시점까지 코드는 세 가지 동시성 모델 모두에 대해 동일합니다. 모든 모델의 채팅 서버는 두 가지 시나리오를 처리해야 합니다.
- 고객의 새로운 연결을 수락합니다.
- 클라이언트로부터 메시지를 수신하고 다른 모든 클라이언트에게 보냅니다.
다중 프로세스 채팅 서버
다중 프로세스 채팅 서버로 이 두 가지 시나리오를 처리하기 위해 클라이언트 연결당 프로세스를 생성할 것입니다. 이 프로세스는 해당 클라이언트에 대해 보내고 받는 모든 메시지를 처리합니다. 원래 서버 프로세스를 분기하여 이러한 프로세스를 만들 수 있습니다.
포킹 프로세스
fork 메서드를 호출하면 프로세스가 있는 것과 똑같은 상태로 현재 프로세스의 복사본이 생성됩니다.
분기된 프로세스에는 자체 프로세스 ID가 있으며 top
과 같은 도구에서 별도로 표시됩니다. 또는 활동 모니터. 다음과 같습니다.
시작하는 프로세스를 마스터 프로세스라고 하고 마스터 프로세스에서 분기된 프로세스를 작업자 프로세스라고 합니다.
이 새로 분기된 작업자 프로세스는 완전히 별개의 프로세스이므로 마스터 프로세스와 메모리를 공유할 수 없습니다. 그들 사이에 소통할 무언가가 필요합니다.
유닉스 파이프
프로세스 간 통신을 위해 Unix 파이프를 사용합니다. Unix 파이프는 두 프로세스 간에 양방향 바이트 스트림을 설정하고 이를 사용하여 한 프로세스에서 다른 프로세스로 데이터를 보낼 수 있습니다. 운 좋게도 Ruby는 이 파이프 주위에 멋진 래퍼를 제공하므로 바퀴를 다시 발명할 필요가 없습니다.
다음 예제에서 우리는 읽기와 쓰기 끝이 있는 Ruby에서 파이프를 설정하고 fork
마스터 프로세스. fork
에 전달되는 블록 내의 코드 분기된 프로세스에서 실행 중입니다. 원래 프로세스는 이 블록 이후에 계속됩니다. 그런 다음 분기된 프로세스에서 원래 프로세스로 메시지를 씁니다.
reader, writer = IO.pipe
fork do
# This is running in the forked process.
writer.puts 'Hello from the forked process'
end
# This is running in the original process, it will puts the
# message from the forked process.
puts reader.gets
파이프를 사용하면 프로세스가 서로 완전히 격리된 경우에도 개별 프로세스 간에 통신할 수 있습니다.
채팅 서버의 구현
먼저 모든 클라이언트와 "작성자"(파이프의 쓰기 끝)에 대한 파이프를 추적하도록 배열을 설정하여 클라이언트와 통신할 수 있습니다. 그런 다음 클라이언트에서 들어오는 모든 메시지가 다른 모든 클라이언트로 전송되는지 확인합니다.
client_writers = []
master_reader, master_writer = IO.pipe
write_incoming_messages_to_child_processes(master_reader, client_writers)
write_incoming_messages_to_child_processes
의 구현을 찾을 수 있습니다. 작동 방식에 대한 세부 정보를 보려면 GitHub에서.
새 연결 수락
들어오는 연결을 수락하고 파이프를 설정해야 합니다. 새 작성자는 client_writers
에 푸시됩니다. 정렬. 주 프로세스는 배열을 반복하고 파이프에 작성하여 각 작업자 프로세스에 메시지를 보낼 수 있습니다.
그런 다음 마스터 프로세스를 분기하면 분기된 작업자 프로세스 내의 코드가 클라이언트 연결을 처리합니다.
loop do
while socket = server.accept
# Create a client reader and writer so that the master
# process can write messages back to us.
client_reader, client_writer = IO.pipe
# Put the client writer on the list of writers so the
# master process can write to them.
client_writers.push(client_writer)
# Fork child process, everything in the fork block
# only runs in the child process.
fork do
# Handle connection
end
end
end
클라이언트 연결 처리
클라이언트 연결도 처리해야 합니다.
분기된 프로세스는 클라이언트에서 닉네임을 가져오는 것으로 시작됩니다(클라이언트는 기본적으로 닉네임을 보냅니다). 그 후 write_incoming_messages_to_client
에서 스레드를 시작합니다. 메인 프로세스의 메시지를 수신합니다.
마지막으로 분기된 프로세스는 들어오는 메시지를 수신 대기하고 마스터 프로세스로 보내는 루프를 시작합니다. 마스터 프로세스는 다른 작업자 프로세스가 메시지를 수신하도록 합니다.
nickname = read_line_from(socket)
puts "#{Process.pid}: Accepted connection from #{nickname}"
write_incoming_messages_to_client(nickname, client_reader, socket)
# Read incoming messages from the client.
while incoming = read_line_from(socket)
master_writer.puts "#{nickname}: #{incoming}"
end
puts "#{Process.pid}: Disconnected #{nickname}"
작동하는 채팅 시스템
이제 전체 채팅 시스템이 작동합니다! 그러나 보시다시피 다중 처리를 사용하는 프로그램을 작성하는 것은 상당히 복잡하고 많은 리소스를 사용합니다. 매우 튼튼하다는 것이 장점입니다. 자식 프로세스 중 하나가 충돌하면 나머지 시스템은 계속 작동합니다. 예제 코드를 실행하고 kill -9 <process-id>
를 실행하여 시도할 수 있습니다. 프로세스 중 하나에서(서버의 로그 출력에서 프로세스 ID를 찾을 수 있음).
다음 기사에서는 스레드만 사용하여 동일한 채팅 시스템을 구현하므로 하나의 프로세스와 더 적은 메모리를 사용하여 동일한 기능을 가진 서버를 실행할 수 있습니다.