Lập trình Socket cơ bản

Tram Ho

1. Struct address

1.1. Struct sockaddr{}

Cấu trúc này sử dụng hầu hết trong các system call ở phần 2, trong đó:

  • sa_family là address family, có dạng AF_xxxx, chủ yếu ta sử dụng AF_INET
  • sa_data[]: lưu trữ địa chỉ đích và cổng

1.2. Struct sockaddr_in{}

Là một cấu trúc song song với struct sockaddr{}, bởi vì việc lưu và lấy dữ liệu trong struct sockaddr{} khá phức tạp, ta sử dụng cấu trúc này.

Đối với 1 số func yêu cầu cấu trúc trên, có thể sử dụng cấu trúc sockaddr_in sau đó tiến hành ép kiểu.

1.3. Struct hostent{}

Cấu trúc lưu trữ dữ liệu host, chủ yếu sử dụng trong việc chuyển đổi giữa ip address và host (DNS), sử dụng ở phần 4 – Chuyển đổi giữa Host name và IP address. Đặc trưng là 2 function sau:

struct hostent* gethostbyname(const char* hostname)

struct hostent* gethostbyaddr(const char* addr, size_t len, int family)

2. Xử lý dữ liệu

2.1. Chuyển đổi giữa host byte và network byte

Dữ liệu được lưu trữ theo 2 kiểu Big-endian và Little-endian.

Trong lập trình socket, có 2 tên gọi lưu trữ dữ liệu là Host byte order và Network byte order(IP), tương ứng với Little-endian và Big-endian. Sử dụng Network byte order đối với các dữ liệu cần truyền qua các máy chủ khác nhau. Vì vậy cần chuyển giữa 2 kiểu dữ liệu này bằng các function sau:

  • htons() — “Host to Network Short”
  • htonl() — “Host to Network Long”
  • ntohs() — “Network to Host Short”
  • ntohl() — “Network to Host Long”

Trong đó Short thực hiện chuyển đổi với loại 2 bytes (sử dụng chuyển đổi cho port), Long là 4 bytes (sử dụng chuyển đổi cho network)

Ví dụ mình có port 64, cần lưu trữ trong biến sin_port trong struct sockaddr_sin, và đương nhiên cần được lưu ở dạng Network byte order vì dữ liệu này được truyền đi.

64 có giá trị hex là 0x40, hay ở dạng 2 bytes được viết là 0x0040. Thực hiện chuyển đổi sin_addr = htons(64), hàm này thực hiện chuyển từ little qua big, vậy giá trị hex của nó sẽ là 0x4000 và có giá trị là 16384. Đó là lý do khi mình run chương trình sau sẽ được kết quả là 16384. Hiểu đơn giản hơn cả 2 kiểu chuyển đổi đều làm đảo byte, tuy nhiên ta có 2 loại func khác nhau để hiểu rõ loại chuyển đổi.

2.2. Chuyển đổi string host thành host address và ngược lại

inet_addr()

inet_aton()

Hàm này chuyển đổi const char *cp (string address) thành dạng numbers-and-dots, lưu trữ trong struct in_addr mà ta sử dụng trong struct sockaddr_in

Example:

inet_ntoa

Chuyển đổi từ dạng numbers_and_dots thành string address

Example

3. System calls

Nguồn tham khảo: https://www.gta.ufrj.br/ensino/eel878/sockets/syscalls.html

3.1. Socket()

Tạo 1 socket, trả về file descriptor, có giá trị nhỏ nhất mà chưa được sử dụng. Bằng -1 nếu thất bại.

  • domain: sử dụng PF_INET
  • type: SOCK_STREAM hoặc SOCK_DGRAM
  • protocol: 0 để tự động chọn giao thức thích hợp

Xem thêm bằng: man socket

3.2. Bind()

Khi 1 socket được tạo, chưa có 1 giá trị nào được gán cho socket (ipaddress, port), bind() sử dụng để làm điều đó. Xem thêm tại man bind

  • sockfd: sock file descriptor
  • struct sockaddr *my_addr:
  • addrlen : size của address, có thể sử dụng sizeof(struct sockaddr)

Example:

3.3. Connect()

Kết nối với 1 máy chủ từ xa

  • sockfd: sock file desciptor
  • struct sockaddr *serv_addr: Cấu trúc lưu địa chỉ và cổng đích kết nối
  • addrlen: size địa chỉ, sử dụng sizeof(struct sockaddr)

Hàm này sẽ trả về giá trị -1 nếu gặp lỗi.

Example:

3.4. Listen()

Đợi các kết nối từ xa đến và xử lý

  • sockfd: sock file descriptor
  • backlog: số kết nối được gửi đến trên hàng chờ đợi được xử lý

Trả về fd hoặc giá trị -1 nếu gặp lỗi

Vì hàm listen() nhận kết nối từ 1 host khác, nên cần thiết lập port mà ta nhận vào. Vì vậy quá trình sẽ là:

  • socket() => bind() => listen() => accept()

Chúng ta sẽ nói về hàm accept() ở phần dưới.

3.5. Accept()

Chấp nhận kết nối tới và trả về 1 file desciptor (cùng giá trị với fd của listen), sử dụng để gửi và nhận data với 2 func send() và recv().

  • sockfd: fd của listen()
  • struct sockaddr *addr: con trỏ trỏ đến cấu trúc lưu trữ địa chỉ struct sockaddr_in
  • socklen_t *addrlen: con trỏ trỏ đến biến lưu trữ sizeof(struct sockaddr_in)

Trả về fd với sử dụng cho send() và recv() ở phần dưới

Example

3.6. Send() và Recv()

Send()

  • sockfd: fd của host muốn gửi dữ liệu, có thể là fd được trả về bởi socket() hoặc accept()
  • const void *msg: data cần gửi
  • int len: Độ lớn của dữ liệu tính theo byte
  • int flags: Đặt giá trị bằng 0

Hàm trả về số byte được gửi đi, gửi hết nếu số byte nhở hơn 1K. So sánh giá trị trả về với int len để biết dữ liệu gửi hết chưa và xử lý phần còn lại.

Recv()

Nhận dữ liệu được gửi đến

  • sockfd: fd của nơi gửi
  • void *buf: buffer chứa dữ liệu
  • int len: độ lớn dữ liệu tối đa nhận vào
  • flags đặt thành 0

Hàm trả về -1 nếu xảy ra lỗi, 0 nếu kết nối bị đóng và giá trị khác là số byte nhận được

3.7. Close() và shutdown()

Close()

close(sockfd)

  • sockfd: fd muốn đóng kết nối, ngưng gửi và nhận dữ liệu

shutdown()

  • sockfd: fd muốn đóng kết nối
  • how: cách đóng (0-ngưng nhận, 1-ngưng gửi, 2-ngưng nhận và gửi)

4. Chuyển đổi giữa Host name và IP address

5. Xử lý lỗi trong socket

Để in lỗi trong quá trình kết nối hay sử dụng các function, ta sử dụng hàm perror()herror() trong thư viện errno.h

Ví dụ khi gặp lỗi trả về -1 trong hàm socket(), ta thực hiện in lỗi bằng hàm perror(“socket”);
Example:

Đổi với các function xử lý DNS (ở phần 4), sử dụng hàm herror để in ra lỗi.
Example:

Chia sẻ bài viết ngay

Nguồn bài viết : Viblo