【Linux Socket 编程入门】04 - socket编程最常使用的函数及数据结构

友情提醒:可以利用上面的目录,选择感兴趣的部分。

(一) 背景

前面3个小节,主要介绍了socket是什么,网络模型,IP地址等基本的知识,接下来介绍socket编程中,最常使用到的几个系统函数(syscall)和相关的数据结构,为linux socket编程入门做好准备。


(二) 常用的数据结构


socket文件描述符(Socket descriptor)

socket 文件描述符就是一个 int类型的整数。socket文件描述符类似于文件句柄,对文件句柄的读写就代表对文件的读写,同理,socket文件描述符代表的是一个带有目的信息的(IP 地址,端口号等)字节流,对socket文件描述符进行读写就代表对某个具体的IP地址,端口号进行读写。socket文件描述符的数据类型为:

int

addrinfo

这个结构是进行socket编程会遇到的第一个结构,它主要用来记录一些地址类型,端口号,socket类型等参数,为后续的函数调用做准备。它主要作为getaddrinfo()的参数。它的结构如下:

struct addrinfo {
	int ai_flags; // AI_PASSIVE, AI_CANONNAME, etc.
	int ai_family; // AF_INET, AF_INET6, AF_UNSPEC
	int ai_socktype; // SOCK_STREAM, SOCK_DGRAM
	int ai_protocol; // use 0 for "any"
	size_t ai_addrlen; // size of ai_addr in bytes
	struct sockaddr *ai_addr; // struct sockaddr_in or _in6
	char *ai_canonname; // full canonical hostname
	struct addrinfo *ai_next; // linked list, next node
};

其中:

  • ai_flags: 填AI_PASSIVE表示不需要手动指定IP地址,由系统帮忙指定IP地址,通常作为服务器端程序的时候,会使用这个flag,在调用bind()时,由系统自动填入本机IP地址。AI_CANONNAME flag会使得ai_canonname被填充为真正的主机名。当然还有很多其他的flag,可以通过man page查看。
  • ai_family:指定是IPv4还是IPv6. AF_INET 代表IPv4, AF_INET6代表ipv6,AF_UNSPEC代表由系统自动选择。
  • ai_next:addrinfo 是一个链表结构,函数getaddrinfo()返回的是一个链表,这意味着你可能会有多个选择。例如,对于同一个主机名:www.baidu.com,可能会对应多个IP地址,getaddrinfo()返回的就是所有的这些IP地址。

sockaddr

在addrinfo结构中,有一个sockaddr结构。这个结构主要存储各种sock address(IPv4 or IPv6)信息,它的定义如下:
struct sockaddr {
unsigned short sa_family; // address family, AF_xxx
char sa_data[14]; // 14 bytes of protocol address
};
其中:
  • sa_family可以有多种值,最常用的是AF_INET 和 AF_INET6.
  • sa_data:包含了目的地址和port 信息。但是这个结构很少用,因为它非常不直观,你需要自己进行转换才能获取IP 地址和port号。

 sockaddr_in

由于sockaddr使用不直观,因此,针对IPv4,增加了这个结构,这个结构体的内部排列与sockaddr是一样的。因此可以用类型的强制转换,将sockaddr强制转换为sockaddr_in.
在sock编程中,会经常使用这个结构体来获取IP address和port号。他的定义如下:
struct sockaddr_in {
short int sin_family; // Address family, AF_INET
unsigned short int sin_port; // Port number
struct in_addr sin_addr; // Internet address
unsigned char sin_zero[8]; // Same size as struct sockaddr
};
其中的struct in_addr 结构为:
struct in_addr {
uint32_t s_addr; // that's a 32-bit int (4 bytes)
};
其中:
  • sin_family:与sockaddr中的sa_family一样,这里设为AF_INET。
  • sin_port:端口号。
  • sin_addr:目的地的IP address。
  • sin_zero[8]:是padding,需要利用memset全部设置为0.

sockaddr_in6

与sockaddr_in一样,这个结构主要针对IPv6,他的定义如下:
struct sockaddr_in6 {
u_int16_t sin6_family; // address family, AF_INET6
u_int16_t sin6_port; // port number, Network Byte Order
u_int32_t sin6_flowinfo; // IPv6 flow information
struct in6_addr sin6_addr; // IPv6 address
u_int32_t sin6_scope_id; // Scope ID
};
struct in6_addr {
unsigned char s6_addr[16]; // IPv6 address
};
注:对于socket入门,可以暂时忽略 sin6_scope_id。

sockaddr_storage

有时候,你无法提前预知到底是IPv4还是IPv6,有没有一个结构体足够大,可以装得下IPv4的信息,同时也装得下IPv6的信息呢?答案是:有。它就是sockaddr_storage。它的定义如下:
struct sockaddr_storage {
sa_family_t ss_family; // address family
// all this is padding, implementation specific, ignore it:
char __ss_pad1[_SS_PAD1SIZE];
int64_t __ss_align;
char __ss_pad2[_SS_PAD2SIZE];
};
根据ss_family为AF_INET 还是 AF_INET6,把sockaddr_storage强制转换为sockaddr_in 或者 sockaddr_in6。sockaddr_storage的用法,在下一篇代码分析里面可以看到。

(三) 常用的函数


inet_pton()

这个函数主要用来将数字和点号表示的IP地址,转化成in_addr 或者in_addr6的结构。“pton” 表示 “presentation to network”。它的用法如下:
struct sockaddr_in sa; // IPv4
struct sockaddr_in6 sa6; // IPv6
inet_pton(AF_INET, "10.12.110.57", &(sa.sin_addr)); // IPv4
inet_pton(AF_INET6, "2001:db8:63b3:1::3490", &(sa6.sin6_addr)); // IPv6
注意:这个函数失败的话,会返回-1或者0,记得要检查返回值,确保返回值是大于0的。

 inet_ntop()

与inet_pton()相反,这个函数将IP地址转换成用数字和点号表示的字符串。“ntop” 代表 “network to presentation,它的用法如下:
// IPv4:
char ip4[INET_ADDRSTRLEN]; // space to hold the IPv4 string
struct sockaddr_in sa; // pretend this is loaded with something
inet_ntop(AF_INET, &(sa.sin_addr), ip4, INET_ADDRSTRLEN);
printf("The IPv4 address is: %s\n", ip4);
// IPv6:
char ip6[INET6_ADDRSTRLEN]; // space to hold the IPv6 string
struct sockaddr_in6 sa6; // pretend this is loaded with something
inet_ntop(AF_INET6, &(sa6.sin6_addr), ip6, INET6_ADDRSTRLEN);
printf("The address is: %s\n", ip6);
注意:INET_ADDRSTRLEN 是一个预定义的宏,代表IPv4字符串长度。INET6_ADDRSTRLEN则代表IPv6的长度。
 

getaddrinfo()

这个函数是第一个会调用到的函数,它的作用主要是通过传入一些必要的参数,让系统自动填充struct addrinfo,供后续函数使用。它的函数原型如下:
#include 
#include 
#include 
int getaddrinfo(const char *node, // e.g. "www.example.com" or IP
                const char *service, // e.g. "http" or port number
                const struct addrinfo *hints,
                struct addrinfo **res);
其中:
  • 前面三个是输入参数,最后一个是输出参数,供后续函数使用。
  • node:通常是主机名或者IP地址。
  • service:service的取值可以很多,我们最常用的是填入端口(port)号。其他值通常是某个service的名字,如"http","fpt","telnet","smtp"等。
Sample:
int status;
struct addrinfo hints;
struct addrinfo *servinfo; // 这个是输出参数
memset(&hints, 0, sizeof hints); // 很重要,一定要先将structure清0
hints.ai_family = AF_UNSPEC; // 这个参数表明不关心是IPv4还是IPv6,由系统自动选择。
hints.ai_socktype = SOCK_STREAM; // 采用TCP的socket
hints.ai_flags = AI_PASSIVE; // 由系统自动填充IP地址。
if ((status = getaddrinfo(NULL, "3490", &hints, &servinfo)) != 0) {
    fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
    exit(1);
}
// servinfo 现在指向的是一个链表,对应着一个或多个IP地址。
// ... 
freeaddrinfo(servinfo); // 最后记得要把servinfo 释放掉。

socket()

这个函数主要用来获取socket文件描述符(socket file descriptor),它的函数原型如下:
#include 
#include 
int socket(int domain, int type, int protocol);
其中:
  • domain:PF_INET或者PF_INET6。在实际编程中,我们常常使用由getaddrinfo() 返回的addrinfo->ai_family.
  • type:SOCK_STREAM or SOCK_DGRAM,在实际编程中,常常使用由getaddrinfo() 返回的addrinfo->ai_socktype.
  • protocol:0 或者通过 getprotobyname() 获取protocol的名字。在实际编程中,常常使用由getaddrinfo() 返回的addrinfo->ai_protocol.
  • 返回值:-1表明出错。正常情况返回一个int型的socket文件描述符。
Sample:
int s;
struct addrinfo hints, *res;
//...
getaddrinfo("www.example.com", "http", &hints, &res);
s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

bind()

这个函数主要用来绑定端口号,它的原型如下:
#include 
#include 
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
其中:
  • sockfd:是由socket()获得的文件描述符。
  • my_addr:包含自己的IP地址,端口号等信息的sockaddr 结构。编程中,常用getaddrinfo() 返回的addrinfo->ai_addr。
  • addr_len:address的长度,编程中常用getaddrinfo() 返回的addrinfo->ai_addrlen。
  • 返回值:-1表示出错。
Sample:
struct addrinfo hints, *res;
int sockfd;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
getaddrinfo(NULL, "3490", &hints, &res);
// make a socket:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
// bind it to the port we passed in to getaddrinfo():
bind(sockfd, res->ai_addr, res->ai_addrlen);

connect()

这个函数通常用在客户端程序,主动发起连接server的请求。通常需要知道server端的IP地址和端口信息。它的函数原型为:
#include 
#include 
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
其中:
  • sockfd:socket文件描述符。
  • serv_addr:server的IP地址信息。编程中,常用getaddrinfo() 返回的addrinfo->ai_addr。
  • addrlen:address的长度,编程中常用getaddrinfo() 返回的addrinfo->ai_addrlen。
  • 返回值:-1表示出错。
Sample:
struct addrinfo hints, *res;
int sockfd;

memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
getaddrinfo("www.example.com", "3490", &hints, &res);
// make a socket:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
// connect!
connect(sockfd, res->ai_addr, res->ai_addrlen);

listen()

这个函数用来告诉别人,你不会主动连接(connect)别人,别人可以来连接你了。常常与accept()连用。它的函数原型如下:
#include 
#include 
int listen(int sockfd, int backlog);
 
     
 
    
其中:
  • sockfd:socket文件描述符。
  • backlog:最大的listen的数量。在accepte()之前,所有连接你的人都会在一个queue里面等待,这个参数指定这个queue的最大值。
  • 返回值:-1表示出错。
Sample:这里只是展示一个调用的流程,具体的例子参见accept()函数。
getaddrinfo();
socket();
bind();
listen();
/* accept() goes here */

accept()

这个函数通常与listen搭配使用。一旦呼叫这个函数,这个函数会一直blocking(或者说sleep)直到有人连接(connect),一旦有人连接你,这个函数便会返回。它的函数原型如下:
#include 
#include 
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
其中:
  • sockfd:socket文件描述符。
  • addr:对方的IP地址信息。编程中,常用sockaddr_storage来保存这个信息,后面再通过类型转换获取对方信息。
  • addrlen:address的长度,编程中常用sizeof(struct sockaddr_storage) 。注意,这个参数仍然是一个输出参数,如果实际的地址长度比sizeof(struct sockaddr_storage)的小,addrlen会被修改成实际的地址长度。
  • 返回值:-1表示出错。正常情况会返回一个新的socket 文件描述符。注意区分第一个参数sockfd与返回值。这两个是不同的文件描述符。为什么会返回一个新的文件描述符呢?因为listen可以允许多个客户端的连接,用的都是同一个sockfd,因此,针对每一个连接,需要一个新的文件描述符来进行后续的读写操作。
Sample:
#include 
#include 
#include 
#include 
#define MYPORT "3490" // the port users will be connecting to
#define BACKLOG 10 // how many pending connections queue will hold
int main(void)
{
    struct sockaddr_storage their_addr;
    socklen_t addr_size;
    struct addrinfo hints, *res;
    int sockfd, new_fd;

    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE; // fill in my IP for me
    getaddrinfo(NULL, MYPORT, &hints, &res);
    // make a socket, bind it, and listen on it:
    sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    bind(sockfd, res->ai_addr, res->ai_addrlen);
    listen(sockfd, BACKLOG);
    // now accept an incoming connection:
    addr_size = sizeof their_addr;
    new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &addr_size);
    // ready to communicate on socket descriptor new_fd!
    .
    .
    .

send() and recv()

这两个函数主要用来进行读写的数据的。它们的函数原型如下:
int send(int sockfd, const void *msg, int len, int flags);
其中:
  • sockfd:为文件描述符。
  • msg:为你要发送的消息。
  • len:消息的长度。
  • flags:填0。
  • 返回值:-1表示错误。正常情况下,返回已发送的数据的长度。注意,真正发送的长度有可能小于你要求的长度。通常情况,小于1K的数据,都会一次发完。
int recv(int sockfd, void *buf, int len, int flags);
其中:
  • sockfd:为文件描述符。
  • buf:为你要接收的消息的buffer。
  • len:buffer的长度。
  • flags:填0。
  • 返回值:-1表示错误。0表示对方已经关闭连接了。正常情况会返回读到的数据的byte数。
Sample:这里展示send()的用法,recv()的用法类似。
char *msg = "Hello World!";
int len, bytes_sent;
...
len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);
.
.

sendto() and recvfrom()

这两个函数主要针对datagram stream的。因为datagram socket不需要首先与远端建立连接,这也是UDP编程模型的特点。它们的函数原型如下:
int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, socklen_t tolen);
其中:
  • 前三个参数与send()一样,移步send()的说明。
  • to:是一个sockaddr的结构体,包含对方的IP地址,端口等信息,编程中,常用getaddrinfo() 返回的addrinfo->ai_addr。
  • to_len:addr的长度,编程中,常用getaddrinfo() 返回的addrinfo->ai_addrlen。
  • 返回值:-1表示错误。正常情况下,返回已发送的数据的长度。注意,真正发送的长度有可能小于你要求的长度。
int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);
其中:
  • 前三个参数,请参考recv。
  • from:通常为一个 sockaddr_storage的结构,用来保存对方的IP地址,端口等信息。
  • fromlen:from的长度。同样,这个长度是一个输出参数,在函数返回后,代表真实的from的长度。
Sample:
我会在后面文章中具体实例里面进行演示。

注意:UDP编程模型仍然可以使用send(), recv()函数。

close()

在不使用某个sock 文件描述符时,记得一定要用close()讲资源释放掉。函数原型如下:
int close(int sockfd);
其中:
  • sockfd是文件描述符
  • 返回值:-1表示错误。正常情况返回0.
Sample:
...
close(fd);


--THE END--

你可能感兴趣的:(网络编程)