Pages

7/13/2012

Basic Bash Shell Scripting for Network Programming

เขียน shell script บน Bash shell ให้ติดต่อกับ Network ได้


Unix, Linux เป็นระบบปฏิบัติการที่ทุกๆอย่างจะอยู่ในรูปไฟล์ทุกอย่าง
หมายความว่า นอกเหนือจากการเข้าไปถึงไฟล์ต่างๆธรรมดาแล้ว  เรายังสามารถเข้าถึงข้อมูลของระบบได้ในรูปแบบของไฟล์ได้ด้วย  เช่น
/proc/cpuinfo    #จะได้สถานะของ CPU ที่รันอยู่ตอนนี้ เช่น มีกี่ core, รันที่กี่ MHz
/proc/meminfo    #จะได้สถานะของ RAM ตอนนี้ว่า มีทั้งหมดเท่าไหร่ ใช้ไปแล้วเท่าไหร่ เหลือเท่าไหร่
/dev/sda  หรือ /dev/hda  #เป็นไฟล์ที่เป็นตัวแทนของ hard disk ตัวแรกของเครื่องขณะนั้น (sda=SATA hard disk, hda=PATA hard disk)

รวมถึงการติดต่อกับ network ก็สามารถทำการติดต่อได้ในรูปแบบนี้
/dev/tcp/host/port    #สำหรับการติดต่อด้วย TCP
/dev/udp/host/port    #สำหรับการติดต่อด้วย UDP
ตัวอย่าง เช่น
/dev/tcp/www.google.com/80
/dev/tcp/127.0.0.1/telnet      #ชื่อ port จะไปแม๊ปกับข้อมูลในไฟล์ /etc/services
/dev/udp/8.8.8.8/53

การติดต่อกับ network แบบนี้ Linux ก็จะมองว่ามันเป็นไฟล์หมดเลย ดังนั้นจึงต้องใช้วิธีการจัดการแบบไฟล์ด้วย

ดังนั้น... มารื้อฟื้นการจัดการไฟล์บน unix กันก่อนละกัน

File Descriptor
การจัดการไฟล์ของ linux จะต้องมีการเปิดไฟล์ และตั้งหมายเลขให้กับแต่ละไฟล์ที่เปิดขึ้นมาก่อน
ยกตัวอย่างเช่น
exec 9<>/etc/passwd
จะเป็นการเปิดไฟล์ /etc/passwd โดยให้ใช้หมายเลข 9 แทนไฟล์ๆนี้  และเป็นการเปิดไฟล์แบบสองทางคือ เปิดเพื่ออ่าน และ เปิดเพื่อเขียนลงไป โดยใช้ไฟล์หมายเลขเดียวกัน
(ปล. ถ้าจะเปิดไฟล์แค่ทางเดียว ให้เปลี่ยนเครื่องหมาย "<>" เป็น:  "<" สำหรับเปิดไฟล์เพื่ออ่านอย่างเดียว หรือ ">" สำหรับเปิดไฟล์เพื่อเขียนลงอย่างเดียว)

การตั้งหมายเลขไฟล์ใหม่ จะใช้หมายเลขอะไรก็ได้ที่เป็นเลขจำนวนเต็มบวก ยกเว้นสามตัวนี้ซึ่งเป็น file descriptor ที่ linux ตั้งมาให้เป็นมาตรฐานอยู่แล้วคือ
   0    หมายถึง standard input  เช่นถ้าเป็นบน console: จะหมายถึงการกด keyboard
   1    หมายถึง standard output  เช่นถ้าเป็นบน console:  จะหมายถึงการแสดงผลออกทางหน้าจอ
   2    หมายถึง standard error  เช่น ไว้แสดงผล error ที่เกิดจากคำสั่งต่างๆ โดยปกติบน console จะหมายถึงสิ่งเดียวกับ 1  (ซึ่งก็คือแสดงออกทางหน้าจอ)

การเล่นกับ File Descriptor

ก่อนอื่นต้องว่าด้วยการทำ redirection ก่อน
Redirection ก็คือ การถ่ายโอนข้อมูลจากคำสั่งนึงๆ หรือ file descriptor อันนึงให้กับตัวอื่นๆ
สิ่งที่น่าจะเคยทำกันแล้วก็คือ
เมื่อต้องการให้ แสดงผลของไฟล์(จากเดิมที่แสดงออกหน้าจอ) ให้แสดงออกลงไฟล์ คือ
ls > /tmp/abc.txt

การใช้ output redirection (เครื่องหมาย ">" )
คือหมายถึงการเอาข้อมูลที่จากเดิมจะแสดงออกทางหน้าจอ(file descriptor หมายเลข 1) ให้ออกลงไฟล์ที่ระบุไว้ด้านหลังเครื่องหมาย >

ดังนั้น คำสั่งข้างบนก็จะเหมือนกับการใช้
ls 1> /tmp/abc.txt

หมายถึงว่า เราสามารถระบุ file descriptor ให้ชัดเจนก่อนเครื่องหมาย ">" (ห้ามมีวรรคระหว่างหมายเลข file descriptor กับ > ) ได้ว่าเราต้องการจะเอาข้อมูลจาก file descriptor นี้ไป redirect ลงไฟล์

จะเห็นได้ว่า ถ้าเราจะใช้เครื่องหมาย > จะต้องตามด้วยไฟล์เสมอ แล้วการ redirection จะทำการเปิดไฟล์ เขียนไฟล์ และปิดไฟล์ให้เองโดยอัตโนมัติ

แต่ว่า ในบางกรณีเราก็ต้องการที่จะทำการเปิดไฟล์เอง ปิดไฟล์เอง และต้องการให้ redirection ทำการโอนข้อมูลเข้ามาในไฟล์ที่เปิดไว้แล้ว(มี file descriptor แล้ว) จะทำยังไง?

ตรงนี้จะเป็นการ redirect ข้อมูลลง file descriptor จะต้องใช้เครื่องหมาย >& แทน
คือถ้ามี & ตามหลังเครื่องหมาย redirection ใดๆ จะแปลว่า จะเป็นการกระทำกับ file descriptor
ยกตัวอย่างเช่น
ls 1>/tmp/abc.txt 2>&1

คำสั่งนี้จะหมายความว่า หลังจากคำสั่ง ls ได้ค่าออกมาแล้ว ให้ standard output(หมายเลขไฟล์=1)เขียนข้อมูลลงในไฟล์ /tmp/abc.txt  หลังจากนั้นจึงให้ standard error (หมายเลขไฟล์=2) ส่งข้อมูลลง standard output (หมายเลขไฟล์=1) ซึ่งตอนนี้ถูกตั้งไว้ให้ชี้ไปที่ไฟล์ /tmp/abc.txt  ดังนั้นจะได้ผลลัพธ์คือ ทั้ง error และผลลัพธ์ปกติ จะถูกเขียนอยู่ในไฟล์ /tmp/abc.txt ทั้งหมด

พอทำ redirection ของฝั่ง output ได้แล้ว(คือสัญลักษณ์ >, >&) มาดูด้านการ input ด้วย redirection บ้าง

การส่งข้อมูลเข้า มีอยู่ด้วยกันสองทางคือ การใช้เครื่องหมาย < และ การใช้ | (อ่านว่า ไปป์ pipe)
การใช้ input redirection (เครื่องหมาย "<")
ก็จะคล้ายกันกับข้างบน ก็คือ จะต้องตามหลังด้วยไฟล์ (ไม่ใช่หมายเลขไฟล์) แล้วระบบการ redirect นี้จะทำการเปิดไฟล์ให้เอง,อ่านไฟล์,ส่งข้อมูลให้กับโปรแกรม,และปิดไฟล์ให้โดยอัตโนมัติ ยกตัวอย่างเช่น
cat < /tmp/abc.txt

ก็จะเอาข้อมูลในไฟล์ /tmp/abc.txt ออกมา และโอนข้อมูลให้กับโปรแกรม cat ซึ่งก็จะแสดงผลออกมาทางหน้าจอ(หมายเลขไฟล์=1)

และเช่นเดียวกันกับเครื่องหมาย output redirection (>) ถ้าต้องการใช้ input แบบเป็นหมายเลขไฟล์ก็จะต้องใช้ & ตามหลังเครื่อง redirection เหมือนกัน เช่น
cat <& 9

ก็จะเป็นการแสดงข้อมูลทั้งหมดของไฟล์ที่ได้เปิดเอาไว้แล้วและตั้งหมายเลข file descriptor ให้ว่าเป็นหมายเลข 9

การใช้ pipe (เครื่องหมาย "|")
ก็เป็นอีกวิธีในการ redirect ข้อมูล output ของโปรแกรมหนึ่ง เพื่อให้กลายมาเป็นข้อมูล input ของอีกโปรแกรมหนึ่ง เช่น
ls | more
หมายความว่า สั่งให้ ls ทำงานไป เมื่อ ls มีข้อมูลที่จะแสดงออก standard output(หมายเลขไฟล์=1) ก็จะให้นำข้อมูลนั้นมาเป็น standard input(หมายเลขไฟล์=0) ของโปรแกรม more ต่อไป ทำให้โปรแกรม more ได้รับข้อมูลที่โปรแกรม ls แสดงผลออกมาได้โดยตรง โดยไม่ต้องมีการพักข้อมูลลงไฟล์ก่อนแล้วค่อยอ่านขึ้นมาอีกที

การปิดไฟล์
เนื่องจากเมื่อเปิดไฟล์ขึ้นมาแล้ว แต่ละ file descriptor นั้น จะสามารถรับส่งข้อมูลได้สองทางหรือทางเดียว ขึ้นอยู่กับตอนที่เปิดไฟล์ ดังนั้นการปิดไฟล์ก็จะเลือกปิดได้สองวิธี(หรือต้องปิดทั้งสองวิธี) คือ
exec 9<&-      # ปิดช่องทาง input เข้าไฟล์หมายเลข 9
exec 9>&-      # ปิดช่องทาง output ออกจากไฟล์หมายเลข 9

คราวนี้ พอได้พื้นฐานของการเล่นกับไฟล์บน linux แล้ว ก็จะเอาทุกอย่างมาประยุกต์เพื่อเล่นกับ network กันละ

อย่างที่บอกไว้ในตอนแรกว่า  การเชื่อมต่อผ่านทาง network ของ linux ก็สามารถมองให้เป็นการใช้ไฟล์ๆนึงได้
โดยการเขียนเข้าไฟล์ หมายถึง การส่งข้อมูลผ่าน network
และการอ่านข้อมูลจากไฟล์ ก็หมายถึง การรับข้อมูลจากทาง network มาประมวลผลต่อ

ยกตัวอย่างโค้ดอย่างง่ายๆ ก่อนละกัน  โค้ดต่อไปนี้เป็นการติดต่อไปที่ www.google.com ผ่านทาง port 80 เพื่อดึงหน้าเวปหน้าแรกออกมา โดยคำสั่งจะเป็น

exec 7 <> /dev/tcp/www.google.com/80
echo -e "GET / HTTP/1.0\n\n" >& 7
while read line <& 7
do
   echo -n $line
done
exec 7<&-
exec 7>&-


มาดูกันทีละคำสั่งละกัน
     exec 7 <> /dev/tcp/www.google.com/80   #เป็นการเปิดช่องทางแบบ TCP ติดต่อไปหา www.google.com ด้วย port 80 โดยจะให้ใช้ไฟล์หมายเลข 7 แทนช่องทางการติดต่อนี้

     echo -e "GET / HTTP/1.0\n\n" >& 7    # คำสั่งนี้เป็นคำสั่งที่ให้ส่งคำสั่ง HTTP ว่า ขอดึงข้อมูลจากหน้าแรก ("/") หน่อย  โดยใช้ echo ส่งคำสั่งนี้ไปใส่ในไฟล์หมายเลข 7 ซึ่งก็คือช่องทางติดต่อ www.google.com ที่เราเปิดเอาไว้แล้ว   และการใช้ "-e" หลังคำสั่ง echo คือการให้คำสั่ง echo แปลความหมายของ "\n\n" ว่าเป็นการขึ้นบรรทัดใหม่ด้วย ไม่ใช่ว่าต้องการจะแสดงออกมาเป็น "\"+"n"+"\"+"n"

    read line <& 7        # คำสั่ง read นี้เป็นการอ่านข้อมูลจากไฟล์หมายเลข 7 มาทีละบรรทัด และเก็บข้อมูลบรรทัดที่อ่านมาได้นี้ลงในตัวแปรที่ชื่อ line  (เวลานำไปใช้ต้องใส่เครื่องหมาย "$" นำหน้าด้วย เป็น "$line")

    while ... do ......... done    # เป็นคำสั่งวนรอบ โดยจะวนซ้ำ ทำคำสั่งที่อยู่ระหว่าง do....done จนกว่า คำสั่ง read นั้นจะให้ผลลัพธ์ออกมาเป็นเท็จ(แปลว่า ไม่มีข้อมูลเหลือในไฟล์หมายเลข 7 ให้อ่านแล้ว) แล้วจึงลงมาทำคำสั่งที่ถัดจาก done ต่อไป

    echo -n $line         # แสดงค่าที่เก็บอยู่ในตัวแปร line ออกมา โดยมี "-n" เพื่อบอกให้ echo ไม่ต้องเพิ่มการบรรทัดใหม่ต่อท้ายข้อมูลให้ (ปกติ echo จะมีการเพิ่มการขึ้นบรรทัดใหม่ให้หนึ่งครั้งหลังจากพิมพ์ข้อมูลเสร็จ)

   exec 7<&-  และ exec 7>&-    # ก็คือการปิดการใช้งานไฟล์หมายเลข 7  ซึ่งก็มีผลเท่ากับการจบการติดต่อกับ www.google.com ด้วย


จบเพียงแค่นี้ดีกว่า  ลองเล่นกันดูละกันนะคับ

---- อัพเดต ----
 - แก้ไขข้อผิดพลาด ls 2>&1 1>/tmp/abc.txt  เป็น ls 1>/tmp/abc.txt 2>&1 แทน

2 comments:

  1. คำสั่ง ls 2>&1 1>/tmp/abc.txt

    ผลลัพธ์ไม่ใช่ ทั้ง error และผลลัพธ์ปกติ จะถูกเขียนอยู่ในไฟล์ /tmp/abc.txt ทั้งหมด
    แต่เป็น error จะแสดงผลทาง standard output และผลลัพธ์ปกติ จะถูกเขียนอยู่ในไฟล์ /tmp/abc.txt

    อ้างอิง: [ http://www.unix.com/shell-programming-scripting/125947-difference-between-dev-null-2-1-2-1-dev-null.html ]

    ReplyDelete
    Replies
    1. แก้ไขละครับ
      เปลี่ยนเป็น ls 1>/tmp/abc.txt 2>&1 แทน

      Delete