Socket Programming
BSD Sockets
Last updated
BSD Sockets
Last updated
Assoc. Prof. Wiroon Sriborrirux, Founder of Advance Innovation Center (AIC) and Bangsaen Design House (BDH), Electrical Engineering Department, Faculty of Engineering, Burapha University
เริ่มต้นตั้งแต่ระบบปฏิบัติการ Berkeley UNIX (BSD) รุ่น 4.2 โดย BSD ได้ถูกปล่อยออกสู่สาธารณะ ก็มีความสามารถในการรองรับรูปแบบการสื่อสารระหว่างโปรเซสและระบบเครือข่ายด้วยวิธีการแบบ socket communication ซึ่งแนวคิดของ Socket คือเพื่อให้การสื่อสารระหว่างโปรเซสดำเนินการผ่านระบบไฟล์ I/O โดยมีการใช้ file descriptor ร่วมกัน โดยทั่วไปแล้วตัวโปรเซสลูกที่เกิดขึ้นจะได้รับสืบทอดตัว file descriptors มาจากโปรเซสแม่ เฉกเช่นเดียวกับกลไกการทำงานของ pipe แต่การสื่อสารด้วยวิธีการแบบ pipe เองจะเป็นแบบทิศทางเดียว (single direction) และก็สื่อสารกันได้เพียงภายในเครื่องคอมพิวเตอร์เครื่องเดียวกันเท่านั้น
ดังนั้นระบบปฏิบัติการ Berkeley UNIX ก็ได้นำเสนอแนวคิดการสื่อสารระหว่างโปรเซสที่สามารถสื่อสารกันภายในเครื่องคอมพิวเตอร์เดียวกัน หรือต่างเครื่องคอมพิวเตอร์ได้แต่จะต้องอยู่ภายใต้ระบบเครือข่าย TCP/IP (TCP/IP networking) รวมทั้งยังรองรับการสื่อสารแบบสองทิศทาง (bidirectional) ได้เช่นกัน ดังนั้นเพื่อให้การสื่อสารสามารถคุยได้สองทิศทางระหว่างโปรเซสแม่และโปรเซสลูกภายในเครื่องคอมพิวเตอร์เดียวกัน สามารถทำได้โดยการเรียกใช้ฟังก์ชัน socketpair() ซึ่งเป็นฟังก์ชันที่ถูกให้ทำงานได้เพียงโดเมนเดียว ซึ่งถูกเรียกว่า UNIX domain โดยการการใช้ AF_UNIX
(Address Format UNIX) และ SOCK_STREAM
ที่อยู่ในไลบรารี sys/socket.h
และไลบรารี sys/types.h
ดังตัวอย่างโปรแกรมข้างล่างนี้
จากตัวอย่างโปรแกรมข้างต้นจะเห็นว่าการใช้ socketpairs
(หรือแม้กระทั่ง pipes) นั้นถูกจำกัดในเรื่องของการสื่อสารข้อมูลระหว่างโปรเซสได้เฉพาะกลุ่มโปรเซสที่อยู่ในตระกูลเดียวกัน (โปรเซสแม่ที่มีการสร้างโปรเซสลูกเพิ่มขึ้นมา) โดยเมื่อไหร่ก็ตามที่มีการเกิดของแต่ละโปรเซสที่ไม่ได้อยู่ในตระกูลเดียวกันหรือโปรเซสที่ทำงานอยู่คนละเครื่องคอมพิวเตอร์แล้ว แต่ละโปรเซสจะสื่อสารกันทันทีไม่ได้ แต่จะต้องสร้าง socket ของตัวเองขึ้นมาเพื่อเอาไว้ส่งและรับข้อมูล โดยจะต้องมีการระบุชื่อ (name) ให้กับ socket เพื่อใช้ในการอ้างอิงถึงกัน เมื่อเริ่มใช้งานชื่อเหล่านั้นจะต้องถูกแปลงให้เป็นหมายเลขที่อยู่ (address) ซึ่งเลขที่อยู่ของแต่ละ socket จะถูกระบุให้อยู่ภายในพื้นที่หรือโดเมนเดียวกัน
ชื่อโดเมนสำหรับ socket มีอยู่หลายแบบ แต่ที่เป็นที่รู้จักและนิยมก็มีอยู่ 2 ประเภทคือ UNIX domain (AF_UNIX) และ Internet domain (AF_INET)
โดยเฉพาะในกรณีที่โปรเซสทำงานแยกกันอยู่คนละเครื่องคอมพิวเตอร์ที่ซึ่งถูกเชื่อมต่ออยู่บนระบบเครือข่ายอินเทอร์เน็ท (TCP/IP) ต้องการจะสื่อสารข้อมูลระหว่างกัน แต่ละโปรเซสจะต้องมีการสร้างชื่อสำหรับ socket ขึ้นมาภายใน Internet domain เดียวกันซึ่งถูกพัฒนาอยู่ในระบบปฏิบัติการ UNIX ที่มีการใช้โปรโตคอลมาตราฐานสำหรับระบบเครือข่ายที่ถูกกำหนดโดยหน่วยงาน DAPRA ได้แก่ IP, TCP และ UDP โดยที่หมายเลขที่อยู่ (address) ใน Internet domain นั้นจะประกอบไปด้วยที่อยู่เครือข่ายของเครื่อง (machine network address) และ หมายเลขพอร์ต (port address)
Datagram Socket
เรียกอีกชื่อหนึ่งว่า Connection less Socket ซึ่งใช้โปรโตคอล UDP (User Datagram Protocol) เป็นตัวกำหนดวิธีการสื่อสาร โดยข้อมูลหรือแพ็กเก็ต (packet) แต่ละตัวจะถูกส่งบน datagram socket ที่แยกเส้นทางกันออกไป ดังนั้นแพ็กเก็ตที่ส่งจากเครื่องต้นทางก็จะถูกลำเลียงกระจายออกไปในแต่ละเส้นทาง จนถึงเครื่องรับปลายทาง โดยแต่ละแพ็กเก็ตก็อาจจะมาถึงเครื่องปลายทางแบบไม่ได้เรียงตามลำดับตามที่ถูกส่งออกจากเครื่องต้นทางก่อนหน้านั้น
Stream Socket
เรียกอีกชื่อหนึ่งว่า Connection-oriented Socket ซึ่งใช้โปรโตคอล TCP (Transport Control Protocol) เป็นตัวกำหนดวิธีการสื่อสาร ซึ่งจะมีการสถาปนาการเชื่อมต่อและการันตีการรับส่งแพ็กเก็ต ดังนั้นข้อมูลหรือแพ็กเก็ตแต่ละตัวจะถูกส่งบน stream socket และจะถูกลำเลียงส่งผ่านช่องทางที่ถูกสร้างขึ้นมานี้ไปยังเครื่องปลายทางจนครบถ้วนสมบูรณ์
Raw Socket
ส่วนใหญ่จะพบในอุปกรณ์เครือข่ายเช่น สวิทซ์ (Switch) และ เราเตอร์ (Router) ซึ่งทำงานอยู่ในระดับ Internet Layer ที่มีการรับส่งข้อมูลโดยไม่ได้มีการใช้โปรโตคอลเหมือน datagram และ stream socket ในการกำหนดมาตราฐานการสื่อสาร
เมื่อโปรเซสทั้งสองที่อยู่ต่างเครื่องคอมพิวเตอร์ต้องการสื่อสารระหว่างกันจะต้องมีสถาปนาการเชื่อมต่อด้วยขั้นตอนตามเทคโนโลยี TCP/IP ซึ่งอยู่ในระดับชั้น transport (transport layer) และจะต้องมีการระบุหมายเลขพอร์ต (port address) ไปยังโปรโตคอลแอพพิเคชั่น (application protocol) ที่โปรเซสแต่ละฝั่งใช้อยู่ด้วย ตัวอย่างเช่นโปรแกรมรับส่งไฟล์ที่ใช้โปรโตคอล FTP ในการกำหนดควบคุมวิธีการส่งไฟล์ระหว่างกัน เรียกการพัฒนาโปรแกรมทางด้านนี้ว่า socket programming
การพัฒนาโปรแกรมทางด้าน socket จะต้องทำความเข้าใจวิธีการตั้งค่าหมายเลข IP address แต่ละตัว ไม่ว่าจะเป็นหมายเลขเครือข่าย (network ID) หมายเลขเครื่อง (host ID) และหมายเลข netmask address
ความสัมพันธ์ระหว่างโปรแกรม, socket, protocol และหมายเลขพอร์ตภายในเครื่องคอมพิวเตอร์เพื่อใช้ในการสื่อสารข้อมูลระหว่างโปรเซสที่อยู่ต่างเครื่องกัน ประเด็นที่น่าสนใจที่นักพัฒนาควรรู้ได้แก่
โปรแกรมหนึ่งโปรแกรมสามารถเปิดซ็อกเก็ตได้หลายซ็อกเก็ตเพื่อรับการเชื่อมต่อจากเครื่องภายนอกได้พร้อมกันในเวลาเดียวกัน
โปรแกรมหลายโปรแกรมสามารถใช้ซ็อกเก็ตตัวเดียวกันในเวลาเดียวกันแต่ไม่ค่อยพบเห็นการใช้ในลักษณะนี้
มากกว่าหนึ่งซ็อกเก็ตที่สามารถถูกเกี่ยวข้องและใช้งานพอร์ตอันเดียวกัน
โปรแกรมประยุกต์แต่ละตัวจะมีการใช้ทั้ง TCP และ UDP เพื่อใช้ในการจัดการและรับส่งข้อมูล
ตามวัตถุประสงค์ที่แตกต่างกันไป เช่น ต้องการเน้นความเร็วในการส่งแม้ข้อมูลจะหายได้บ้างก็จะใช้ช่องทาง UDP หรือถ้าเน้นความถูกต้องของข้อมูลโดยที่ข้อมูลจะต้องไม่สูญหายก็จะใช้ช่องทาง TCP ในการรับส่งแทน
ไฟล์ /etc/services ภายในระบบปฏิบัติการลีนุกซ์จะบอกรายละเอียดของโปรแกรมให้บริการและโปรโตคอลที่ใช้หมายเลขพอร์ตในการสื่อสารผ่านซ็ิอกเก็ตในระดับของ Transport layer
ฟังก์ชันและตัวแปรที่เกี่ยวข้อง
ไลบรารีที่เกี่ยวข้องคือ sys/types.h
,sys/socket.h
,unistd.h
,netinet/in.h
และsys/un.h
ข้อมูลตัวแปรที่สำคัญ
ตัวแปร struct sockaddr
สำหรับระบุข้อมูลพื้นฐานของโดเมนทั้ง AF_UNIX
และ AF_INET
ตัวแปร struct sockaddr_in
สำหรับระบุคุณลักษณะของ socket ใน Internet domain โดยมีรายละเอียดดังนี้
ฟังก์ชัน int socket(int domain, int communication_type, int protocol);
เพื่อใช้สร้าง socket โดยระบุโดเมนที่ต้องการ (domain) เช่น AF_INET
และระบุประเภทของการสื่อสาร (communication_type) เช่น SOCK_STREAM
โดยโปรโตคอลก็ขึ้นอยู่กับชนิดของโดเมน
การสื่อสารแบบมีการสถาปนาการเชื่อมต่อ (connection-oriented หรือ connectionful) หมายถึงก่อนที่จะมีการสื่อสารข้อมูลระหว่างกันทั้งสองเครื่องจะต้องมีการสถาปนาการเชื่อมต่อให้เสร็จเรียบร้อยก่อน (point-to-point) เพื่อให้การสื่อสารข้อมูลผ่านท่อนี้มีการส่งข้อมูลที่ครบถ้วนและมีถูกต้องของข้อมูลมากที่สุด โดยมีขั้นตอนคร่าวๆดังต่อไปนี้
สร้าง socket และระบุโดเมน -- socket()
ทำการผูกรายละเอียดของ socket และหมายเลขพอร์ต -- bind()
ทำการเปิดพอร์ตเพื่อรอการขอการเชื่อมต่อจากภายนอก -- listen()
เมื่อมีการร้องขอการเชื่อมต่อก็จะตอบรับและเตรียมสถาปนาการเชื่อมต่อระหว่างกัน -- accept()
เริ่มการสื่อสารข้อมูลระหว่างกัน -- write()
and read()
สิ้นสุดการเชื่อมต่อ -- close()
การสื่อสารแบบไม่ต้องมีการสถาปนาการเชื่อมต่อ (connectionless) หมายถึงเครื่องคอมพิวเตอร์สามารถสื่อสารข้อมูลออกไปได้ทันทีโดยที่ไม่ต้องมีการสถาปนาการเชื่อมต่อแต่อย่างใด นักพัฒนาโปรแกรมก็จะต้องหาวิธีการเทคนิคต่างๆเพื่อทำให้การสื่อสารข้อมูลถูกต้องด้วยตัวเอง โดยมีขั้นตอนคร่าวๆดังต่อไปนี้
สร้าง socket และระบุโดเมน -- socket()
ทำการผูกรายละเอียดของ socket และหมายเลขพอร์ต -- bind()
เริ่มการสื่อสารข้อมูลระหว่างกันได้ทันที -- sendto()
and recvfrom()
สิ้นสุดการเชื่อมต่อ -- close()
โดยค่าปริยายแล้วการทำงานของ sockets จะเป็นในลักษณะแบบถูกกำหนดให้หยุดรอ (blocking) ดังนั้นฟังก์ชันต่างๆจะต้องถูกหยุดอยู่ชั่วขณะจนกว่างานที่ร้องขอบน socket จะสิ้นสุดลง แต่อย่างไรก็ตามก็สามารถตั้งค่าให้ socket ทำงานแบบ non-blocking ได้โดยการใช้ฟังก์ชัน fcntl
ตัวอย่างโปรแกรมสื่อสารผ่าน socket ในลักษณะการเชื่อมต่อแบบ connectionless (SOCK_DGRAM) โดยเครื่องลูก (datagram client) จะเชื่อมต่อไปยังเครื่องแม่ (datagram server) แล้วทำการส่งข้อความไปยังเครื่องแม่ เมื่อเครื่องแม่ได้รับข้อความก็จะแสดงออกทางหน้าจอของดังตัวอย่างโปรแกรมข้างล่างนี้
โปรแกรมฝั่งเครื่องแม่ (Server):
โปรแกรมฝั่งเครื่องลูก (Client):
ทำการคอมไพล์และรันโปรแกรมโดยแยกหน้าต่าง terminal ดังนี้
ตัวอย่างโปรแกรมเชื่อมต่อผ่านซ็อกเก็ตในลักษณะ connection-oriented (SOCK_STREAM
) เมื่อทำการสถาปนาการเชื่อมต่อสำเร็จโดยเครื่องลูก (client) จะเชื่อมต่อไปยังเครื่องแม่ (server) แล้วทำการส่งข้อความไปยังเครื่องแม่ เมื่อเครื่องแม่ได้รับข้อความก็จะแสดงออกทางหน้าจอของดังตัวอย่างโปรแกรมข้างล่างนี้
โปรแกรมฝั่งเครื่องแม่ (Server):
โปรแกรมฝั่งเครื่องลูก (Client):
ทำการคอมไพล์และรันโปรแกรมโดยแยกหน้าต่าง terminal ดังนี้
จากการทำงานของโปรแกรมข้างต้น เมื่อโปรแกรม strm_server
เริ่มทำงานบนเครื่องแม่แล้ว จะแจ้งหมายเลขพอร์ตที่เปิดรอให้เครื่องอื่นเข้ามาเชื่อมต่อ หลังจากนั้นเมื่อรันโปรแกรม strm_client
ที่อยู่ในฝั่งของเครื่องลูกโดยระบุอาร์กิวเมนต์ตัวแรกเป็นหมายเลขที่อยู่ของเครื่องแม่ (127.0.0.1
) และตัวที่สองเป็นหมายเลขพอร์ตที่ถูกเปิดในเครื่องแม่ (37219
) จนกระทั่งการเชื่อมต่อระหว่างทั้งสองเครื่องถูกสถาปนาเรียบร้อยแล้ว ข้อความที่ถูกกำหนดไว้ในโปรแกรม strm_client ก็จะถูกส่งไปยังเครื่องแม่อัตโนมัติดังผลลัพธ์ข้างบน
ฟังก์ชัน accept()
ภายในโปรแกรม strm_server
จะถูกให้หยุดรอฟังการร้องขอการเชื่อมต่อจากเครื่องคอมพิวเตอร์ภายนอก ซึ่งถ้ายังไม่มีการร้องขอการเชื่อมต่อหรือข้อมูลที่อ่านได้มาจาก socket ยังมาไม่ครบ ณ เวลานั้นตัวโปรแกรม strm_server
ก็ยังคงหยุดรอโดยไม่สามารถไปทำงานอย่างอื่นได้เลย
ดังนั้นเพื่อให้การทำงานของโปรแกรม strm_server
กลายเป็นแบบ non-blocking กล่าวคือโปรแกรมยังสามารถทำงานอย่างอื่นได้อยู่ โดยที่ไม่ต้องหยุดรอเหมือนโปรแกรมข้างต้น ด้วยการเรียกใช้ฟังก์ชัน select()
เพื่อทำการตรวจสอบว่าเมื่อใดที่มีการร้องขอการเชื่อมต่อ socket จากเครื่องภายนอก ก็จะค่อยไปเรียกฟังก์ชัน accept()
ให้ทำงานทันที ด้วยวิธีการนี้เองสามารถประยุกต์ให้โปรแกรมบนเครื่องแม่ สามารถเปิดรับการเชื่อมต่อได้มากกว่าหนึ่ง socket ดังตัวอย่างโปรแกรม new_strm_server.c
ที่ปรับปรุงใหม่ข้างล่างนี้
ทำการคอมไพล์และรันโปรแกรมอีกครั้งโดยแยกหน้าต่าง terminal ดังนี้