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

来源:互联网 发布:mysql 更改character 编辑:程序博客网 时间:2024/05/21 17:35

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

(一) 背景

前面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_UNSPECint ai_socktype; // SOCK_STREAM, SOCK_DGRAMint ai_protocol; // use 0 for "any"size_t ai_addrlen; // size of ai_addr in bytesstruct sockaddr *ai_addr; // struct sockaddr_in or _in6char *ai_canonname; // full canonical hostnamestruct 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_xxxchar 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_INETunsigned short int sin_port; // Port numberstruct in_addr sin_addr; // Internet addressunsigned 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_INET6u_int16_t sin6_port; // port number, Network Byte Orderu_int32_t sin6_flowinfo; // IPv6 flow informationstruct in6_addr sin6_addr; // IPv6 addressu_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; // IPv4struct sockaddr_in6 sa6; // IPv6inet_pton(AF_INET, "10.12.110.57", &(sa.sin_addr)); // IPv4inet_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 stringstruct sockaddr_in sa; // pretend this is loaded with somethinginet_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 stringstruct sockaddr_in6 sa6; // pretend this is loaded with somethinginet_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 <sys/types.h>#include <sys/socket.h>#include <netdb.h>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清0hints.ai_family = AF_UNSPEC; // 这个参数表明不关心是IPv4还是IPv6,由系统自动选择。hints.ai_socktype = SOCK_STREAM; // 采用TCP的sockethints.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 <sys/types.h>#include <sys/socket.h>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 <sys/types.h>#include <sys/socket.h>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, whicheverhints.ai_socktype = SOCK_STREAM;hints.ai_flags = AI_PASSIVE; // fill in my IP for megetaddrinfo(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 <sys/types.h>#include <sys/socket.h>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 <sys/types.h>#include <sys/socket.h>
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 <sys/types.h>#include <sys/socket.h>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 <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 holdint 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--

0 0
原创粉丝点击