+8

Basic Socket Programming

1. Struct address

1.1. Struct sockaddr{}

struct sockaddr {
    unsigned short    sa_family;    // address family, AF_xxx
    char              sa_data[14];  // 14 bytes of protocol address
};

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{}

struct sockaddr_in {
    short int          sin_family;  // Address family
    unsigned short int sin_port;    // Port number
    struct in_addr     sin_addr;    // Internet address
    unsigned char      sin_zero[8]; // Same size as struct sockaddr
}; 
// Internet address (a structure for historical reasons)
struct in_addr {
    unsigned long s_addr; // that's a 32-bit long, or 4 bytes
}; 

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.

int sockfd;
struct sockaddr_in my_addr;

sockfd = socket(PF_INET, SOCK_STREAM, 0); // do some error checking!

my_addr.sin_family = AF_INET;         // host byte order
my_addr.sin_port = htons(MYPORT);     // short, network byte order
my_addr.sin_addr.s_addr = inet_addr("10.12.110.57");
memset(&(my_addr.sin_zero), '\0', 8); // zero the rest of the struct

// int bind(int sockfd, struct sockaddr *my_addr, int addrlen); 
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));

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)

struct hostent {
               char  *h_name;            /* official name of host */
               char **h_aliases;         /* alias list */
               int    h_addrtype;        /* host address type */
               int    h_length;          /* length of address */
               char **h_addr_list;       /* list of addresses */
           }

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. Đọc thêm về 2 kiểu dữ liệu này 2 đây: https://viblo.asia/p/little-endian-vs-big-endian-E375z0pWZGW

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 Numbers and dots và ngược lại

Đầu tiên chúng ta cần hiểu rõ về 2 dạng string host và numbers and dots. String host tức là dạng địa chỉ ở dạng string, ví dụ như "192.168.1.1", hoặc 1 host "scanme.nmap.org". Còn dạng numbers and dots chỉ đơn giản là 1 chuỗi số nguyên dạng unsigned long, ví dụ như dưới đây, mình có chương trình như sau

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int main(){
   unsigned long a = inet_addr("192.168.1.1");
   printf("%x",a); #0x101a8c0
   printf("%b", a); #1000000011010100011000000
   printf("%d", a); #16885952
}

Như vậy bởi vì lưu ở dạng unsigned long nên ta có thể dễ dàng chuyển qua hex hoặc binary để biểu diễn địa chỉ IP của nó.

inet_addr()

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

struct sockaddr_in my_addr
my_addr.sin_addr.s_addr = inet_addr("192.168.1.1"); => 192.168.1.1 (Network byte order - Big endian)

inet_aton()

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int inet_aton(const char *cp, struct in_addr *inp);

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:

struct sockaddr_in my_addr;

my_addr.sin_family = AF_INET;         // host byte order
my_addr.sin_port = htons(MYPORT);     // short, network byte order
inet_aton("10.12.110.57", &(my_addr.sin_addr));
memset(&(my_addr.sin_zero), '\0', 8); // zero the rest of the struct 

inet_ntoa

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

char* inet_ntoa(struct in_addr inp);

Example

string *addr = inet_ntoa(my_addr.sin_addr);

3. System calls

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

3.1. Socket()

#include <sys/types.h>
#include <sys/socket.h> //để sử dụng socket()

int socket(int domain, int type, int protocol); 

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

#include <sys/types.h>
#include <sys/socket.h>

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

Example:

#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MYPORT 3490

main()
{
    int sockfd;
    struct sockaddr_in my_addr;

    sockfd = socket(PF_INET, SOCK_STREAM, 0); // do some error checking!

    my_addr.sin_family = AF_INET;         // host byte order
    my_addr.sin_port = htons(MYPORT);     // short, network byte order
    my_addr.sin_addr.s_addr = inet_addr("10.12.110.57");
    memset(&(my_addr.sin_zero), '\0', 8); // zero the rest of the struct

    // don't forget your error checking for bind():
    bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
}

3.3. Connect()

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

#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen); 
  • 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:

#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define DEST_IP   "10.12.110.57"
#define DEST_PORT 23

main()
{
    int sockfd;
    struct sockaddr_in dest_addr;   // will hold the destination addr

    sockfd = socket(PF_INET, SOCK_STREAM, 0); // do some error checking!

    dest_addr.sin_family = AF_INET;          // host byte order
    dest_addr.sin_port = htons(DEST_PORT);   // short, network byte order
    dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
    memset(&(dest_addr.sin_zero), '\0', 8);  // zero the rest of the struct

    // don't forget to error check the connect()!
    connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));
}

3.4. Listen()

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

#include <sys/types.h>
#include <sys/socket.h>

int listen(int sockfd, int backlog); 
  • 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().

#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 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

#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MYPORT 3490    // the port users will be connecting to

#define BACKLOG 10     // how many pending connections queue will hold

main()
{
    int sockfd, new_fd;  // listen on sock_fd, new connection on new_fd
    struct sockaddr_in my_addr;    // my address information
    struct sockaddr_in their_addr; // connector's address information
    int sin_size;

    sockfd = socket(PF_INET, SOCK_STREAM, 0); // do some error checking!

    my_addr.sin_family = AF_INET;         // host byte order
    my_addr.sin_port = htons(MYPORT);     // short, network byte order
    my_addr.sin_addr.s_addr = INADDR_ANY; // auto-fill with my IP
    memset(&(my_addr.sin_zero), '\0', 8); // zero the rest of the struct

    // don't forget your error checking for these calls:
    bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));

    listen(sockfd, BACKLOG);

    sin_size = sizeof(struct sockaddr_in);
    new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
}

3.6. Send() và Recv()

Send()

int send(int sockfd, const void *msg, int len, int flags);
  • 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

int recv(int sockfd, void *buf, int len, unsigned int flags);
  • 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()

int shutdown(int sockfd, int how); 
  • 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:

if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

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

if ((he=gethostbyname("facebook.com")) == NULL) {  // get the host info 
        herror("gethostbyname");
    }

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí