Linux C网络编程基础

Linux套接字结构定义

       Linux在头文件<sys/socket.h>中定义了一种通用的套接字结构类型,以供不同的协议调用:

struct sockaddr {
    unsigned short int sa_family;
    unsigned char sa_data[14];
};

       其中sa_family是套接字协议族地址类型,sa_data则是具体的协议地址(不同的协议族对应不同的地址结构)。

常见协议对应的sa_family值
可选值 说明
AF_INET IPv4协议
AF_INET6 IPv6协议
AF_LOCAL UNIX协议
AF_LINK 链路地址协议
AF_KEY 密钥套接字

       除了sockaddr以外,Linux还在头文件<netinet/in.h>中定义了另外一种结构类型sockaddr_in,它和sockaddr等效且可以互相转换(需要显式转换),通常在涉及TCP/IP的编程协议中使用。

struct sockaddr_in {
    int sa_len;                     //长度单位,不必设置,通常情况下固定长度为16字节
    short int sa_family;            //地址族
    unsigned short int sin_port;    //端口号
    struct in_addr sin_addr;        //IP地址
    unsigned char sin_zero[8];      //填充0以保持与struct sockaddr同样大小
};

struct sin_addr {
    in_addr_t s_addr;    //32位IPv4地址,网络字节序
};

       使用sockaddr_in的时候需要注意以下几点:

·结构sockaddr_in中的TCP或UDP端口号sin_port和IP地址sin_addr都是以网络字节序存储的,因此要注意格式统一

·32位的IPv4地址可以利用两种不同的方法使用,如sockaddr_in类型的结构体addr,要使用其IP地址可以是addr.sin_addr(此时引用的是结构体类型),也可以是addr.sin_addr.s_addr(此时引用的是整型)

·sin_zero不被使用,它是为了和通用套接字地址sockaddr保持一致而引入的,通常会被填充为0

Linux C的网络基础操作函数

字节顺序转换函数族

       计算机的数据存储有两种形式:大端格式和小端格式

·大端格式:字数据的高位存储在低地址中

·小端格式:字数据的低位存储在高地址中

       网络中的数据传输采用的是大端格式,而计算机操作系统既有大端格式(大型机)也有小端格式(一般PC机)。Linux在头文件<arpa/inet.h>提供了4个函数用于处理大端格式和小端格式的数据转换。

/*
    函数名中,h代表host,n代表network,s代表short,l代表long
    32位的long数据通常存放IP地址,16位的short数据通常存放端口号
    这些函数调用成功后返回处理后的值,调用失败则返回-1
*/
uint32_t htonl(uint32_t hostlong);    //将32位PC机数据(小端格式)转换为32位网络传输数据(大端格式)
uint16_t htons(uint16_t hostshort);   //将16位PC机数据(小端格式)转换为16位网络传输数据(大端格式)
uint32_t ntohl(uint32_t netlong);     //将32位网络传输数据转换为32位PC机数据
uint16_t ntohs(uint16_t netshort);    //将16位网络传输数据转换为16位PC机数据

IP地址转换函数族

       Linux在头文件<arpa/inet.h>提供了用于将“点分十进制”表示的IP地址与二进制表示的IP地址相互转换的函数族,这些函数包括inet_aton、inet_addr和inet_ntoa等。

        ·inet_aton函数用于将点分十进制数的IP地址转换成网络字节序的32位二进制数值。输入的点分十进制IP地址存放在参数straddr中,作为返回结果的二进制数值存放在addrptr中。

int inet_aton(const char *straddr, struct in_addr *addrptr);

       ·inet_ntoa函数调用的结果将作为函数的返回值返回给调用它的函数。

char *inet_ntoa(struct in_addr inaddr);

       ·int_addr函数的功能与inet_aton函数相同,但是结果传递的方式不同。结果以返回值的形式返回。

in_addr_t inet_addr(const char *straddr);

域名转换函数

        Linux在头文件<netdb.h>中定义了一个结构体,用于描述一个主机的相关参数:

#define h_addr h_addr_list[0];    //主机的第一个IP地址
struct hostent {
    char *h_name;          //主机的正式名称
    char *h_aliases;       //主机的别名
    int h_addrtype;        //主机的地址类型,IPv4为AF_INET
    int h_length;          //主机的地址长度,对于IPv4是4字节,即32位
    char **h_addr_list;    //主机的IP地址列表
};

       头文件<netdb.h>提供了gethostbyname和gethostbyaddr函数用于处理域名和地址的转换:

extern int h_errno;
struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);

       gethostbyname用于实现域名或主机名到IP地址的转换,参数name指向存放域名或主机名的字符串。gethostbyaddr用于实现IP地址到域名或主机名的转换,参数addr是一个指向含有地址结构(in_addr)的指针;参数len是此结构的大小,对于IPv4而言其值为4,对于IPv6则是16;参数type为协议类型。

       若调用这两个函数成功,则返回一个指向hostent结构的指针,若调用失败则返回空指针NULL,同时设置全局变量h_errno为相应的值。

h_errno可能的值
h_errno

说明

HOST_NO_FOUND 找不到对应的主机
TRY_AGAIN 出错重试
NO_RECOVERY 出现了不可修复的错误
NO_DATA 该名字有效,但没有找到该记录

Linux网络套接字操作函数

创建套接字描述符函数

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

int socket(int domain, int type, int protocol);    //创建成功的返回值为整型的套接字描述符,创建失败则返回-1

       ·domain:套接字的协议类型,socket函数支持的协议类型如下

协议名称 描述
AF_UNIX, AF_LOCAL 本地交互协议
AF_INET IPv4协议
AF_INET6 IPv6协议
AF_NETLINK 内核接口设备协议
AF_IPX IPX-Novell协议
AF_X25 ITU-T X.25/ISO-8208协议
AF_AX25

业余无线电AX.25协议

AF_ATMPVC 原始ATM接入协议
AF_APPLETALK 苹果公司的Appletalk协议
AF_PACKET 底层数据包接口

      ·type:用于指定当前套接字类型,socket函数支持的套接字类型包括SOCK_STREAM(数据流)、SOCK_DGRAM(数据报)、SOCK_SEQPACKET(顺序数据报)、SOCK_RAW(原始套接字)、SOCK_RDM(可靠传递消息)、SOCK_PACKET(数据包)。

       ·除了在使用原始套接字以外,通常情况下设置为0,以表示使用默认的协议。

       在Linux系统中创建一个套接字时会在内核中创建一个套接字数据结构,然后返回一个套接字描述符标识这个套接字数据结构。这个套接字数据结构包含连接的各种信息,如目的地址、TCP状态以及发送接收缓冲区等。TCP协议这个套接字数据机构的内容来控制这条连接。

绑定套接字函数

       在创建了套接字之后需要将本地地址和套接字绑定在一起,Linux提供了bind函数进行这个操作:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);    //若绑定成功则返回0,否则返回-1

       其中参数sockfd是创建套接字时得到的套接字描述符,addr是本地地址,addrlen是套接字对应的地址结构长度。在C/S模式下服务器和客户端都可以使用bind函数来设置套接字地址,通常来说有以下5种模式:

       ·服务器指定套接字地址的工人端口号,不指定IP地址。这时addr的值设为INADDR_ANY,表示它愿意接收来自任何网络设备接口的客户端连接,这是服务器最经常使用的绑定方式。

       ·服务器指定套接字地址的公认端口号和IP地址,表示服务器只接收来自对应于这个IP地址的特定网络设备端口的客户端连接

       ·客户端指定套接字地址的连接端口号,在一般情况下,客户端不用指定自己的套接字地址的端口号,当客户调用函数connect进行TCP连接时,系统会自动为它选择一个未用的端口号,当客户端调用函数connect进行TCP连接时,系统会自动为它选择一个未使用的端口号,并且用本地的IP地址来填充套接字地址中的相应项。

       ·指定客户端的IP地址和连接端口号,表示客户端使用指定的网络设备接口和端口号进行通信

       ·指定客户端的IP地址,表示客户端使用指定的网络设备接口进行通信,系统自动为客户端选择一个未使用的端口号。一般情况下,只有在主机有多个网络设备接口时使用。

bind函数对应的参数组合方式
C/S IP port 说明
服务器 INADDR_ANY 非0值 指定服务器的公认端口号
服务器 本地IP地址 非0值 指定服务器的IP地址和公认端口号
客户端 INADDR_ANY 非0值 指定客户端的连接端口号
客户端 本地IP地址 非0值 指定客户端的IP地址和连接端口号
客户端 本地IP地址 0 指定客户端的IP地址

       在编写客户端程序时,通常不适用固定的端口号,除非是在必须使用特定端口的情况下,例如:

       ·服务器执行主动关闭操作:服务器最后进入TIME_WAIT状态。当客户机再次与这个服务器进行连接时,仍使用相同的客户机端口号,于是这个连接与前一次连接的套接字对完全相同,这是因为前一次连接处于TIME_WAIT状态,并未完全消失,所以这次连接请求被拒绝,函数connect以错误返回。

       ·客户端执行手动关闭操作:客户端最后进入TIME_WAIT状态,当立刻再次执行这个客户端程序时,客户机将继续与这个固定客户机端口号绑定,但因为前一次连接处于TIME_WAIT状态,并未消失,系统会发现这个端口号仍被占用,所以这次绑定操作失败,函数bind以错误返回。

建立连接函数

       当使用socket函数建立一个套接字并且绑定了地址之后,即可使用connect函数来和服务器建立连接:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);    //若连接成功则返回0,否则返回-1

       connect函数的参数含义与bind函数相同。

       在调用connect函数建立连接之前,客户端应用程序需要制定服务器端进行的套接字地址,而客户端同行不会指定自己的套接字地址,这时Linux会自动从1024~5000的端口范围中为客户端分配一个未被使用的套接字地址,然后将该端口号和本机IP地址结合在一起放入套接字地址中。

       当客户端调用函数connect函数来主动建立连接时,这个函数将启动TCP3次握手过程,在连接建立之后或发生错误时,函数返回。连接过程中可能有以下几种错误情况:

       ·如果客户端TCP协议没有接收到对它的SYN确认信息,则函数以错误返回,错误类型为ETIMEOUT。

       ·如果远程TCP协议返回一个RST数据报,则函数立即以错误返回,错误类型为ECONNREFUSED。当在SYN数据报指定的目的端口号没有服务器进程在等待连接时,会发送RST数据报,向客户机报告这个错误。

      ·如果客户机的SYN数据报导致某个路由器产生“目的地不可达”类型的ICMP消息,则函数以错误返回,错误类型为EHOSTUNREACH或ENETUNREACH。通常情况下,TCP协议在接收到ICMP消息后回记录下这个消息并继续发送几次SYN数据报,如果都宣告失败,函数才会以错误返回。

       如果调用函数connect失败,应该用函数close关闭这个套接字,不能再次用这个套接字来调用函数connect。

倾听套接字切换函数

       对于服务器端的应用程序而言,在创建了套接字之后通常需要等待客户端的连接,此时可以使用listen函数将该套接字转换为倾听套接字。    

int listen(int sockfd, int backlog);    //成功则返回0,否则返回-1

       参数sockfd为套接字描述符,backlog为设置请求队列的最大长度。该函数的功能有以下两个:

       ·socket函数创建的套接字是主动套接字,可以用它来进行主动连接(调用connect函数),但不能接受连接请求,而用于服务器的套接字必须能够接收客户端请求。listen函数将一个尚未连接的主动套接字转换成为一个被动套接字,被动套接字可以接收请求。

       总结起来就是,若要创建一个倾听套接字,必须首先调用socket函数创建一个主动套接字,然后调用bind函数将套接字与服务器套接字地址绑定在一起,最后调用listen函数进行转换。这3个操作是所有TCP服务器必须执行的操作。

       而对于参数backlog的作用,TCP协议为每个倾听套接字维护两个队列:

       ·未完成连接队列:每个尚未完成3次握手操作的TCP连接在这个队列中占有一项。TCP协议在接收到一个客户机的SYN数据报后,在这个队列中创建一个新条目,然后发送确认消息以及自己的SYN数据报(ACK+SYN),等待客户端对自己的SYN进行确认。此时,套接字处于SYN_RCVD状态,这个条目保存在队列中,直到客户端返回对自己SYN消息的确认,或者连接超时。

       ·已完成连接队列:每个已经完成3次握手操作,但尚未被应用程序接收(调用accept函数)的TCP连接在这个队列中占有一项。

       参数backlog指定倾听套接字的完成连接对了的最大长度,表示这个套接字能够接收的最大数目的未接收(unaccepted)连接。如果当一个客户端的SYN消息到达,但这个队列已满,TCP协议将会忽略这个SYN,并且不会发送RST数据报。

接收连接函数

       当服务器倾听到一个连接之后,可以使用函数accept从倾听套接字的完成连接队列中接收一个连接,如果这个完成连接队列为空,则会使得这个进程进入睡眠状态。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

       当这个函数执行成功时,函数返回值为一个新的套接字描述符,标识这个接收的连接。而且函数还把参数addr指向的结构变量中的地址修改为客户机地址,更新参数addrlen指向的整型变量中存储客户机地址的长度。如果对客户机的地址和长度都不感兴趣,可以将参数addr和addrlen设置为NULL。若函数执行失败,返回-1。

       accept函数从倾听套接字的完成连接队列中接收一个已经建立的TCP连接,因为倾听套接字时专为接收客户机请求,完成3次握手的连接操作而用的,所以TCP协议不能使用倾听套接字来标识这个连接,于是TCP协议创建一个新的套接字来标识这个要接收的连接,并将它返回给应用程序。现在有两个套接字:一个是调用accept函数时使用的倾听套接字,另一个是accept函数执行成功返回的连接套接字(connected socket)。这两个套接字的作用是完全不同的:一个服务器进程通常只需要创建一个倾听套接字,在服务器进程的整个活动期间,用它来接收所有客户机连接请求,在服务器进程终止前关闭这个倾听套接字;而对于每个接收的连接,TCP都要创建一个新的连接套接字来标识这个连接,服务器使用这个连接套接字与客户机进行通信,当服务器处理完这个客户机请求时,关闭这个套接字。

关闭连接函数

       当操作完成之后,可以使用close函数来关闭当前建立的连接,这函数定义在头文件<unistd.h>中。

int close(int fd);

       close函数将套接字描述符的引用计数减1。如果描述符的引用计数大于0,则表示还有进程引用这个描述符,close函数正常返回;如果描述符的引用计数变为0,则表示再没有进程引用这个描述符,于是启动清除套接字描述符的操作,函数正常返回。清除操作是将这个套接字描述符标记为关闭状态,进程将不再能访问这个套接字,但并不表示套接字已经被删除(联想一下TCP的4次挥手的过程)。TCP将继续使用这个套接字,将尚未发送的数据传递给对方,然后发送FIN数据报,执行关闭操作,直到这个TCP连接完全关闭后,套接字才会被删除。

套接字读写函数

       函数read和write分别用于从套接字读/写数据。

int read(int fd, char *buf, int len);
int write(int fd, char *buf, int len);

       fd为套接字描述符;buf指定数据缓冲区;len指定接收或发送的数据量大小(以字节为单位)。函数成功执行时,返回读/写成功的数据量大小,失败则返回-1。

套接字地址获取函数

       当需要获取套接字的地址时,可以使用头文件<sys/socket.h>中的getsockname和getpeername函数,前者用于返回本地的套接字地址,后者用于返回与本地套接字建立了连接的对等套接字地址。

//函数调用成功则返回0,否则返回-1
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

       需要注意的是,如果sockfd标记的套接字中的地址长度大于addr指向的对象的存储空间,则存储的地址将被截断,如果socket还没有捆绑地址,则结果是未知的。

       存储在addr参数所指对象中的地址格式依赖于该套接字的通信域。对于给定的通信域,套接字地址的长度通常是固定的,如果需要确切知道空间大小,并提供实际需要的存储空间,通常的做法是利用与套接字通信域相匹配的数据类型为addr所指对象分配空间,然后强制其地址转换为struct socket *并传送给getsockname。

       getsockname函数通常会应用于以下情况:

       ·对于没有使用bind捆绑地址至套接字的客户进程,在它成功调用connect之后,getsockname可以返回内核指定给该套接字的本地地址(如IP地址和端口号等)。

       ·当利用0端口号(系统自动分配端口号)调用bind之后,getsockname可以返回系统指定给该套接字的本地端口号

       ·getsockname可以获得一个套接字的地址族

       ·服务进程在接收了客户的连接请求之后(成功调用accept之后),以accept返回的描述符调用getsockname可以获得指定给该连接的套接字地址,这个套接字是连接套接字而不是倾听套接字。

       getpeername函数的行为与getsockname函数类似。

发送和接收函数

       除了之前说的read和write之外,还可以使用recv和send函数来在套接字中实现数据的发送和接送。这两个函数相比read/write多了一个参数用以指明控制套接字特殊传输方式的各种标志。

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

       与read/write函数相同,如果调用成功,返回值是代表接收/发送成功的字节数(函数发送的实际长度可能小于其指定的长度),失败则返回-1。flags参数用于指定消息的传送类型,当该值为0时send函数和write函数完全相同,同样recv函数与read函数也相同。

       send函数的flags参数除0外还有如下两种取值:

send函数的flags参数取值
MSG_OOB send函数发送的数据成为带外数据,带外数据是流套接字特有的。在流套接字上传送数据时,数据按它们写出的顺序传送。因为接收进程必须依次读套接字上的当前数据,因此当出现一个紧急情况时,没有办法立即通知接收进程。带外数据正是用于解决这个问题。带外数据在正常的数据流之外发送,其效果相当于越过套接字上所有等待的数据。当它到达接收进程时,接收进程会收到一个信号,从而进程可以立即处理这个数据。
MSG_DONTROUTE 不在消息中包含路由信息,通常来说普通应用不会关系相应的信息。

       recv函数的flags参数除0外还有如下三种取值:

recv函数的flags参数取值
MSG_OOB 读带外数据
MSG_PEEK 窥视套接字上的数据而不实际读出它们,即尽管buffer所指对象中填入了所请求的数据,但随后的read或recv将读到相同的数据。
MSG_WAITALL 请求函数阻塞直至所请求的全部数据都已接收到。

       write函数、read函数、send函数和recv函数都是用于TCP下面向连接的套接字数据发送和接收。而在UDP下面向无连接的套接字数据发送和接收则需要使用sendto和recvfrom函数。

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *src_addr, socklen_t addrlen);

                                                                                                   本文内容摘自《Linux C编程从基础到实践》,有改动

猜你喜欢

转载自blog.csdn.net/qq_37653144/article/details/81605294