5.1 socket地址API
大端字节序:高字节存储在低地址处,反之亦然。
小端字节序:高字节存储在高地址处,反之亦然。
由于不同的主机可能使用不同的字节序,为了使数据能在两台使用不同字节序的主机之间直接传递,需要统一发送出去的数据的格式:
发送端总是把要发送的数据转化成
大端字节序
后再发送,接收端知道对方发过来的数据是大端字节序,所以接收端可以根据自身采用的字节序来决定是否对接收到的数据进行转换
。因此,大端字节序也称为
网络字节序;
由于现在主机基本都采用小端字节序,所以小端字节序又称为
主机字节序。
Linux提供以下4个函数实现主机字节序和网络字节序间的相互转换:
#include <netinet/in.h>
//长整型主机字节序转换为网络字节序 (host to net, long)
unsigned long int htonl( unsigned long int hostlong );
//短整型主机字节序转换为网络字节序 (host to net, short)
unsigned short int htons( unsigned short int hostshort );
//长整型网络字节序转换为主机字节序
unsigned long int ntohl( unsigned long int netlong );
//短整型网络字节序转换为主机字节序
unsigned short int ntohs( unsigned short int netshort );
这4个函数中,长整型函数通常用来转换
IP地址
,短整型函数用来转换
端口号
。
5.1.1 通用socket地址(不是主要学习的)
通用socket地址的结构体有
两个
。
第一个
sockaddr
的定义如下:
(所有专用socket地址类型都要转为sockaddr)
#include <bits/socket.h>
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
};
sa_family_t:地址簇类型
sa_data:存放socket地址值
协议簇通常与地址簇一一对应,如下表:
协议簇 |
地址簇 |
sa_data存储的值的含义和长度 |
PF_UNIX (UNIX本地簇协议) | AF_UNIX |
文件路径名, 长达18字节 |
PF_INET(TCP/IPv4协议簇) | AF_INET |
16位端口号和32位ip地址, 共6字节 |
PF_INET6(TCP/IPv6协议簇) | AF_INET6 |
16位端口号,32位流标识,128位IPv6地址,32位范围ID, 共26字节 |
14字节的sa_data无法容纳上述协议簇的地址值,故Linux定义了下面这个socket地址结构体。
第二个
sockaddr_storage
的定义如下:
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int__ss_align;
char __ss_padding[128 - sizeof(__ss_align)];
};
5.1.2 专用socket地址(主要学习)
TCP/IPv4专用socket地址结构体:
struct sockaddr_in
{
sa_family_t sin_family; //地址簇:AF_INET
u_int16_t sin_port; //端口号:用网络字节序表示
struct in_addr sin_addr; //IPv4地址结构体,见下面
};
struct in_addr
{
u_int32_t s_addr; //IPv4地址值,用网络字节序表示
};
TCP/IPv6专用socket地址结构体:
struct sockaddr_in6
{
sa_family_t sin6_family; //地址簇:AF_INT6
u_int16_t sin6_port; //端口号:用网络字节序表示
u_int32_t sin6_flowinfo; //流信息,设为0
struct in6_addr sin6_arr; //IPv6地址结构体,见下面
u_int32_t sin6_scope_id; //scope ID,尚处于实验阶段
};
struct in6_addr
{
unsigned char sa_addr[16]; //IPv6地址,要用网络字节序表示
};
注意:所有
专用socket
地址(sockaddr_in, sockaddr_in6以及sockaddr_storage)类型的变量在使用时都需要
转化为通用
socket地址类型
sockaddr
(强转即可),因为所有socket编程接口使用的地址参数类型都是sockaddr。
5.1.3 IP地址转化函数
1、
这两个函数是将点分十进制字符串
IPV4
地址转换为网络字节序整数IP地址:
#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr); //失败时返回INADDR_NONE
int inet_aton(const char* cp, struct in_addr* inp); //转化结果保存在inp所指的结构体中。成功返回1,失败返回0
2、
这个函数功能与上面两个函数的功能相反:
#include <arpa/inet.h>
char* inet_ntoa(struct in_addr in);
该函数使用内部的一个静态变量保存转换结果,函数返回值指向这个静态变量,不建议使用。
3、
下面两函数能实现上面三个函数的功能,并且适用于
IPV4
和
IPV6
地址:(
推荐使用
)
#include <arpa/inet.h>
//将点分十进制的字符串IP地址src转换成网络字节序整数IP地址dst;family表示地址簇,可取AF_INET或AF_INET6
//成功返回1,失败返回0并设置errno
int inet_pton(int family, const char* src, void* dst);
p:presentation, n:numeric
//将网络字节序整数IP地址src转换为点分十进制的字符串IP地址dst; len表示存储单元dst的长度
//成功返回指向结果的指针,失败返回NULL
const char* inet_ntop(int family, const void* src, char* dst, socklen_t len);
//可以用宏来替换len
#include <netinet/in.h>
#define INET_ADDRSTRLEN 16 //用于IPV4
#define INET6_ADDRSTRLEN 46 //用于IPV6
使用示例:
char *ipv4 = "192.168.1.1";
struct in_addr ip4;
inet_pton(AF_INET, ipv4, &ip4);
ipv4 = inet_ntop(AF_INET, &ip4, ipv4, INET_ADDRSTRLEN);
printf("%x\n", ip4.s_addr);
5.2 创建socket (client && server) –指定地址簇
#include <sys/types.h>
#include <sys/socket.h>
//创建socket,实际上返回值就是一个文件描述符,后续的所有操作都要依赖这个文件描述符
int socket(int domain, int type, int protocol);
成功返回一个文件描述符,失败返回-1并设置errno.
参数解释:
domain:协议簇,对于TCP/IP而言,可取PF_INET或PF_INET6
type:服务类型,对于TCP/IP而言,可取SOCK_STREAM(流服务,传输层使用TCP)或SOCK_DGRAM(数据服务,传输层使用UDP)
protocol:协议,值几乎由前两个参数决定,默认使用0即可。
5.3 命名socket (client && server) –为socket绑定地址
#include <sys/types.h>
#include <sys/socket.h>
//成功返回0,失败返回-1,并设置errno。
int bind(int server_sockfd, const struct sockaddr* server_addr, socklen_t addrlen);
参数解释:
server_sockfd:socket()返回值
server_addr:保存服务器的IP的端口以及使用的地址簇
addrlen:sockaddr_in结构体长度
errno取值: EACCES,被绑定的地址是受保护地址,仅超级用户能访问
EADDRINUSE,被绑定的地址正在使用中
服务端通常要使用bind,而客服端不需要。操作系统会自动为客户端分配socket地址。
5.4 监听socket(server) –创建监听队列
绑定完地址后,要为socket创建一个监听队列,存放待处理的客户端连接请求。
#include <sys/socket.h>
//成功返回0,失败返回-1并设置errno
int listen(int server_sockfd, int qlen);
参数解释:
server_sockfd: socket()的返回值
qlen: 内核监听队列的最大长度,典型值为5
listen调用会创建一个监听队列存放待处理的客户端连接。
5.5 接受连接(server) –接收连接请求
#include <sys/socket.h>
//成功返回一个新的连接client_sockfd,失败返回-1并设置errno
int accept(int server_sockfd, struct* sockaddr* client_addr, socklen_t* addrlen);
参数解释:
server_sockfd: socket()的返回值
client_addr: 保存客户端地址信息结构体的指针
addrlen:addr结构体的长度
accept调用成功时,函数内部将产生专门用于数据I/O的套接字,并返回其文件描述符。
在调用accept前先调用sleep(20)暂停20s,当客户端执行连接操作后立马断开客户端的网络连接或者立刻退出客户端程序,
发现accept调用正常返回。说明accept调用只是从监听队列中取出连接,而不关心连接处于何种状态。
5.6 发起连接
(client)
#include <sys/types.h>
#include <sys/socket.h>
//成功返回0,失败返回-1并设置errno
int connect(int sockfd, const struct* sockaddr* serv_addr, socklen_t addrlen);
参数解释:
sockfd: socket()的返回值
serv_addr: 保存服务端地址信息结构体的指针
addrlen:serv_addr结构体的长度
5.7 数据读写
5.7.1 TCP数据读写
可以使用用来读写文件的read()和write()。
#include <unistd.h>
//成功返回接收的字节数,失败返回-1并设置errno
ssize_t read(int sockfd, void* buf, size_t bytes);
参数解释:
sockfd: 接收对象的文件描述符
buf: 保存接收数据的缓冲区地址
bytes:要接收的最大字节数
#include <unistd.h>
//成功返回发送的字节数,失败返回-1并设置errno
ssize_t write(int sockfd, void* buf, size_t bytes);
参数解释:
sockfd: 发送对象的文件描述符
buf: 保存发送数据的缓冲区地址
bytes:要发送的字节数
但socket提供专门用于数据读写的接口,
recv()和send()
增加了对数据读写的控制。
#include <sys/types.h>
#include <sys/socket.h>
//成功返回接收到的字节数,返回0表示对方已关闭连接,失败返回-1并设置errno
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
参数解释:
sockfd: 接收对象的文件描述符
buf: 保存接收数据的缓冲区地址
len:接收缓冲区大小
flags:通常设为0
#include <sys/types.h>
#include <sys/socket.h>
//成功返回实际发送的字节数,失败返回-1并设置errno
ssize_t send(int sockfd, void* buf, size_t len, int flags);
参数解释:
sockfd: 发送对象的文件描述符
buf: 保存发送数据的缓冲区地址
len:发送缓冲区大小
flags:通常设为0
flags参数对收发数据做了额外的控制,取值如下表所示:
例如:
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
char data[] = "1234";
char recvData[10];
send(sockfd, data, strlen(data), MSG_OOB);
recv(sockfd, recvData, 10, MSG_OOB);
MSG_OOB表示带外缓存,保存带外缓存的内存只有1字节。
5.7.2 UDP数据读写
recvfrom()和sendto()
#include <sys/types.h>
#include <sys/socket.h>
//成功返回接收到的字节数,返回0表示对方已关闭连接,失败返回-1并设置errno
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
参数解释:
sockfd: 接收对象的文件描述符
buf: 保存接收数据的缓冲区地址
len:可接收的最大字节数
flags:通常设为0
src_addr:存有发送端地址信息的sockaddr结构体变量地址
addrlen:src_addr结构体变量的长度
#include <sys/types.h>
#include <sys/socket.h>
//成功返回实际发送的字节数,失败返回-1并设置errno
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t* addrlen);
参数解释:
sockfd: 发送对象的文件描述符
buf: 保存发送数据的缓冲区地址
len:待发送的数据的长度
flags:通常设为0
dest_addr:存有目标地址信息的sockaddr结构体变量地址
addrlen:dest_addr结构体变量的长度
例如:
int sockfd = socket(PF_INET, SOCK_DGREAM, 0);
struct sockaddr_in serv_addr, clnt_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
const int BUFF_SIZE = 30;
char message[BUFF_SIZE];
socklen_t clnt_addr_sz = sizeof(clnt_addr);
int str_len = recvfrom(sockfd, message, BUFF_SIZE, 0, (struct sockaddr*)&clnt_addr, &clnt_addr_sz);
sendto(sockfd, message, str_len, 0, (struct sockaddr*)&clnt_addr, clnt_addr_sz);
5.7.3 通用数据读写函数
socket提供了一套既适用与TCP,又适用于UDP的数据读写函数recvmsg()和sendmsg()。
#include <sys/socket.h>
//成功返回实际接收的字节数,失败返回-1并设置errno
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
参数解释:
sockfd:文件描述符
msg:msghdr结构体指针,见下文
flags:通常设为0
//成功返回实际发送的字节数,失败返回-1并设置errno
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);
参数解释:
sockfd:文件描述符
msg:msghdr结构体指针,见下文
flags:通常设为0
struct msghdr
{
void* msg_name; //指向socket地址结构体变量,指定通信双方的地址。对于TCP,需设为NULL
socklen_t msg_namelen //socket地址的长度
struct iovec* msg_iov; //分散的内存块
int msg_iovlen; //分散的内存块数量
void* msg_control; //指向辅助数据的起始位置
socklen_t msg_controllen; //辅助数据的大小
int msg_flags; //复制函数中的flags参数,在调用中更新
};
struct iovec
{
void* iov_base; //内存起始地址
size_t iov_len; //这块内存的长度
};
对于recvmsg,数据将被读取并存放在msg_iovlen块分散的内存中,内存的长度和位置由msg_iov指向的数组指定,称为分散读。
对于sendmsg,msg_iovlen分散内存中数据将被一起发送,称为集中写。
5.8 关闭连接
用close()关闭连接。
不过close在关闭连接时只能将socket的读和写同时关闭。
#include <unistd.h>
//成功返回0,失败返回-1并设置errno
int close(int fd);
用shutdown()关闭连接。
shutdown在关闭连接时既能关闭读,又能关闭写,又能将读和写同时关闭。
#include <sys/socket.h>
//成功返回0,失败返回-1并设置errno
int shutdown(int sockfd, int howto);
参数解释:
sockfd: 待关闭的socket文件描述符
howto: 关闭方式
下表是
howto
的取值及对应的含义:
5.9 TCP和UDP连接过程
5.9.1 TCP的连接流程图
TCP套接字的I/O缓冲如下图所示:
TCP套接字的数据收发没有边界
,即发送端调用1次write()传输40字节,接收端可能要调用4次read()接收这40字节数据。
实际上
,write()调用后并非立即传输数据,read()调用后也并非马上接收数据。
就像上图所示,write()调用瞬间,数据将移至输出缓冲区,在适当的时候会传向输入缓冲区;read()调用瞬间,从输入缓冲区读取数据。
TCP的I/O缓冲特性总结为:
(1)I/O缓冲在每个TCP套接字中单独存在。
(2)I/O缓冲在创建套接字时自动生成。
(3)即使调用close()关闭套接字也会继续传递输出缓冲区中遗留的数据。
(4)关闭套接字将丢失输入缓冲区中的数据。
另外也能发现,
TCP套接字是全双工通信
。
下面是客服端向服务端循环发消息,客服端发什么,服务端就打印什么。
服务端server:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int server_sockfd;//服务器端套接字
int client_sockfd;//客户端套接字
int len;
struct sockaddr_in my_addr; //服务器网络地址结构体
struct sockaddr_in remote_addr; //客户端网络地址结构体
socklen_t sin_size;
char buf[BUFSIZ]; //数据传送的缓冲区
memset(&my_addr,0,sizeof(my_addr)); //数据初始化--清零
my_addr.sin_family=AF_INET; //设置为IP通信
my_addr.sin_addr.s_addr=INADDR_ANY;//服务器IP地址--允许连接到所有本地地址上
my_addr.sin_port=htons(8000); //服务器端口号
/*创建服务器端套接字--IPv4协议,面向连接通信,TCP协议*/
if((server_sockfd=socket(PF_INET,SOCK_STREAM,0))<0)
{
perror("socket error");
return 1;
}
/*将套接字绑定到服务器的网络地址上*/
if(bind(server_sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr))<0)
{
perror("bind error");
return 1;
}
/*监听连接请求--监听队列长度为5*/
if(listen(server_sockfd,5)<0)
{
perror("listen error");
return 1;
};
sin_size=sizeof(struct sockaddr_in);
/*等待客户端连接请求到达*/
if((client_sockfd=accept(server_sockfd,(struct sockaddr *)&remote_addr, &sin_size))<0)
{
perror("accept error");
return 1;
}
printf("accept client %s\n",inet_ntoa(remote_addr.sin_addr));
len=send(client_sockfd,"Welcome to my server\n",21,0);//发送欢迎信息
/*接收客户端的数据并将其发送给客户端--recv返回接收到的字节数,send返回发送的字节数*/
while((len=recv(client_sockfd,buf,BUFSIZ,0))>0)
{
buf[len]='\0';
printf("%s\n",buf);
if(send(client_sockfd,buf,len,0)<0)
{
perror("write error");
return 1;
}
}
/*关闭套接字*/
close(client_sockfd);
close(server_sockfd);
return 0;
}
客户端client:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int client_sockfd;
int len;
struct sockaddr_in remote_addr; //服务器端网络地址结构体
char buf[BUFSIZ]; //数据传送的缓冲区
memset(&remote_addr,0,sizeof(remote_addr)); //数据初始化--清零
remote_addr.sin_family=AF_INET; //设置为IP通信
remote_addr.sin_addr.s_addr=inet_addr("127.0.0.1");//服务器IP地址
remote_addr.sin_port=htons(8000); //服务器端口号
/*创建客户端套接字--IPv4协议,面向连接通信,TCP协议*/
if((client_sockfd=socket(PF_INET,SOCK_STREAM,0))<0)
{
perror("socket error");
return 1;
}
/*将套接字绑定到服务器的网络地址上*/
if(connect(client_sockfd,(struct sockaddr *)&remote_addr,sizeof(struct sockaddr))<0)
{
perror("connect error");
return 1;
}
printf("connected to server\n");
len=recv(client_sockfd,buf,BUFSIZ,0);//接收服务器端信息
buf[len]='/0';
printf("%s",buf); //打印服务器端信息
/*循环的发送接收信息并打印接收信息(可以按需发送)--recv返回接收到的字节数,send返回发送的字节数*/
while(1)
{
printf("Enter string to send:");
scanf("%s",buf);
if(!strcmp(buf,"quit"))
break;
len=send(client_sockfd,buf,strlen(buf),0);
len=recv(client_sockfd,buf,BUFSIZ,0);
buf[len]='\0';
printf("received:%s\n",buf);
}
/*关闭套接字*/
close(client_sockfd);
return 0;
}
5.9.2 UDP的连接流程图
UDP是无连接的,所以每次调用sendto()时都需要传递对端的socket地址。系统在调用sendto()时自动分配IP和Port,因此UDP客户端没有地址分配过程。
UDP套接字的数据收是有边界的,因此发送函数和接收函数的调用次数完全一致。
5.9 几个重要的socket API
5.9.1 socket带外标记函数
带外数据用来告知对端,本端发生的重要的事件;优先级比普通数据高,应该总是被立即发送。
#include <sys/socket.h>
//判断sockfd是否处于带外标记,即下一个被读取的数据是否是带外数据,
//如果是,返回1,此时利用MSG_OOB标志的recv()来接收带外数据;如果不是,返回0
int sockatmark(int sockfd);
5.9.2
socket
地址信息函数
下面两个函数用来获取一个socket的本端socket地址,和对端socket地址。
#include <sys/socket.h>
//获取sockfd对应的本端socket地址,成功返回0,失败返回-1并设置errno
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
参数解释:
sockfd:要操作的socket文件描述符
address:指向获取到的本端socket地址结构体
address_len:指向该socket地址的长度
//获取sockfd对应的对端socket地址,成功返回0,失败返回-1并设置errno
int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len);
参数解释:
sockfd:要操作的socket文件描述符
address:指向获取到的对端socket地址结构体
address_len:指向该socket地址的长度
5.9.3
socket
选项函数
下面两个函数专门用来
获取
和
设置
socket的
可选项
。
#include <sys/socket.h>
//获取sockfd的可选项,成功返回0,失败返回-1并设置errno
int getsockopt(int sockfd, int level, int optname, void* opval, socklen_t* optlen);
参数解释:
sockfd:用于查看套接字可选项的文件描述符
level:要查看的可选项协议层
optname:可选项名称
opval:保存查看结果的缓冲地址值
optlen:向第四个参数optval传递的缓冲大小。调用该函数后,该变量中保存通过第四个参数返回的可选项信息的字节数
//设置sockfd的可选项,成功返回0,失败返回-1并设置errno
int setsockopt(int sockfd, int level, int optname, void* opval, socklen_t* optlen);
参数解释:
sockfd:用于更改套接字可选项的文件描述符
level:要设置的可选项协议层
optname:可选项名称
opval:保存要更改的选项信息的缓冲地址值
optlen:向第四个参数optval传递的缓冲大小。
下面是当
optname
取值分别为
【SO_REUSEADDR】、【SO_RCVBUF和SO_SNDBUF】、【SO_RCVLOWAT和SO_SNDLOWAT】、【SO_LINGER】
的示例
:
(1)SO_REUSEADDR
比如说,某个
TCP
socket的端口为12345,该socket处于TIME_WAIT状态,当再创建使用12345端口的socket时会失败。但是,通过setsockopt()调用并令optname取值为SO_REUSEADDR,可以重用成功。
int sock = socket(PF_INET, SOCK_STREAM, 0);
int reuse = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
(2)SO_RCVBUF和SO_SNDBUF
SO_RCVBUF和SO_SNDBUF分别表示
TCP
接收缓冲区和发送缓冲区的大小。用这2个选项可以更改或读取当前I/O缓冲区大小。setsockopt操作会时,系统会将接收缓冲区和发送缓冲区的大小翻倍。通常接收缓冲区最小值为256字节,发送缓冲区最小值为2048字节。
int sock = socket(PF_INET, SOCK_STREAM, 0);
int sendbuf = 2000;
int len = sizeof(sendbuf);
//先设置TCP发送缓冲区大小,然后立即读取之
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, sizeof(sendbuf));
getsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, (socklen_t*)&len);
printf("the tcp sendbuf size after setting is %d.\n", sendbuf);
/*the tcp sendbuf size after setting is 4000.*/
说明TCP发送缓冲取大小被系统扩大了一倍
int recvbuf = 50;
int len = sizeof(recvbuf );
//先设置TCP接收缓冲区大小,然后立即读取之
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &recvbuf , sizeof(recvbuf));
getsockopt(sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, (socklen_t*)&len);
printf("the tcp recvbuf size after setting is %d.\n", recvbuf);
/*the tcp recvbuf size after setting is 256.*/
说明TCP接收缓冲不能小于系统的最小值256
(3)SO_RCVLOWAT和SO_SNDLOWAT
SO_RCVLOWAT和SO_SNDLOWAT分别表示
TCP
接收缓冲区和发送缓冲区的低水位标记。默认情况下,
TCP
接收缓冲区和发送缓冲区的低水位标记均为1个字节。
(4)SO_LINGER
SO_LINGER用于控制close()关闭
TCP
连接的行为。默认情况下,使用close(),close会立即返回,TCP模块负责将发送缓冲区中残留的数据发给对方。
#include <sys/socket.h>
struct linger
{
int l_onoff; //取值非0:开启该功能;取值0:关闭该功能
int l_linger; //延迟关闭的时间
};
(1)l_onoff == 0。SO_LINGER不起作用。
(2)l_onoff != 0 && l_linger == 0。close立刻返回,TCP模块丢发送缓冲区内的数据,同时给对方发送一个复位报文段。
5.9.4 网络信息API
(1)gethostbyname 和 gethostbyaddr
通过域名获取主机信息:gethostbyname ;通过IP地址获取主机信息:gethostbyaddr。
#include <netdb.h>
//成功时返回hostent结构体地址,失败时返回NULL指针
struct hostent* gethostbyname(const char* hostname);
//成功时返回hostent结构体地址,失败时返回NULL指针
struct hostent* gethostbyaddr(const char* addr, socklen_t len, int family);
参数解释:
addr:含有IP地址信息的in_addr结构体指针
len:IP地址信息的字节数,IPv4时为4,IPv6时为16
family:地址簇信息,IPv4时为AF_INET,IPv6时为AF_INET6
struct hostent
{
char* h_name; //官方域名
char** h_aliases; //该域名的其他别名,可能有多个。
int h_addrtype; //地址簇类型。若是IPv4,则为AF_INET
int h_length; //保存IP地址长度。若是IPv4,则为4;若是IPv6,则为16
char** h_addr_list; //以整数形式保存域名对应的IP地址。可能一个域名对应多个IP地址。
};
调用gethostbyname()后返回的
hostent结构体
的变量结构图如下:
结构体成员h_addr_list
指向字符串指针数组(由多个字符串地址构成的数组)。但字符串指针数组中的元素实际指向的是in_addr结构体变量地址值而非字符串。
h_addr_list结构体成员如下图
:
gethostbyname和gethostbyaddr 使用:
gethostbyname(const char* hostname)使用:
char strhostname[] = "www.naver.com";
struct hostent* host = gethostbyname(strhostname);
printf("official name : %s\n", host->h_name);
/*official name : www.g.naver.com*/
int i;
for (i = 0; host->h_aliases[i]; i++)
{
printf("aliases %d : %s\n", i + 1, host->h_aliases[i]);
}
/*aliases 1 : www.naver.com*/
printf("address type : %s\n", (host->h_addrtype==AF_INET)?"AF_INET":"AF_INET6");
/*address type : AF_INET*/
for (i = 0; host->h_addr_list[i]; i++)
{
printf("IP addr %d : %s\n", i + 1, inet_ntoa(*(struct in_addr*)host->h_addr_list[i])); //将整型IP转化为字符串形式
}
/*IP addr 1 : 202.131.29.70
IP addr 2 : 222.122.195.6
*/
gethostbyaddr(const char* addr, socklen_t len, int family)使用:
struct hostent* host;
struct sockaddr_in addr;
addr.sin_addr.s_addr = inet_addr("74.125.19.106");
host = gethostbyaddr((char*)&addr.sin_addr, 4, AF_INET);
printf("official name : %s\n", host->h_name);
/*official name : nuq04s01-in-f106.google.com*/
(2)
getservbyname 和 getservbyport
通过服务名获取服务信息:getservbyname;通过端口获取服务信息:getservbyport。
#include <netdb.h>
//成功时返回servent结构体地址,失败时返回NULL指针
struct servent* getservbyname(const char* name, const char* proto);
参数解释:
name:目标服务的名字
proto:服务类型。"tcp"表示获取流服务;"udp"表示获取数据报服务;NULL表示获取所有类型的服务
//成功时返回servent结构体地址,失败时返回NULL指针
struct servent* getservbyport(int port, const char* proto);
参数解释:
port:目标服务的端口,网络字节序
proto:服务类型。"tcp"表示获取流服务;"udp"表示获取数据报服务;NULL表示获取所有类型的服务
struct servent
{
char* s_name; //服务名称
char** s_aliases; //服务的别名列表,可能有多个
int s_port; //端口号,网络字节序
char* s_proto; //服务类型,通常是tcp或udp
};
获取目标服务器上的daytime服务:
struct servent* servinfo1 = getservbyname("daytime", "tcp");
struct servent* servinfo2 = getservbyport(htons(13568), "tcp");
struct sockaddr_in servaddr;
servaddr.sin_port = servinfo1->s_port;
printf("port=%d\n", ntohs(servinfo1->s_port));
(3)getaddrinfo
getaddrinfo既能通过域名获取IP地址(内部使用的是
gethostbyname
),又能通过服务名获取端口号(内部使用的是
getservbyname
)。
#include <netdb.h>
//成功时返回0,失败时返回错误码
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result);
参数解释:
hostname:可以接收域名或字符串表示的IP地址,如:"127.0.0.1"
service:可以接收服务名或字符串表示的十进制端口号
hints:控制getaddrinfo的输出,可被设置为NULL
result:指向一个链表,链表用于存储getaddrinfo获取的结果
struct addrinfo
{
int ai_flags; //见下图表
int ai_family; //地址簇
int ai_sockype; //服务类型,SOCK_STREAM 或 SOCK_DGRAM
int ai_protocol; //协议,通常设为0
socklen_t ai_addrlen; //socket地址ai_addr的长度
char* ai_canonname; //域名的别名
struct sockaddr* ai_addr; //指向socket地址
struct addrinfo* ai_next; //指向下一个sockinfo结构体对象
};
下图表是
ai_flags
的取值及对应的含义:
使用hints获取”daytime”流服务信息:
struct addrinfo hints;
struct addrinfo* res;
hints.ai_sockype = SOCK_STREAM;
getaddrinfo("host-name", "daytime", &hints, &res);
注意:由于getaddrinfo隐式分配内存,使用后需释放res所指的内存。
#include <netdb.h>
void freeaddrinfo(struct addrinfo* res);
参数解释:
res:指向一个保存有getaddrinfo获取结果的链表
(4)getnameinfo
getnameinfo能通过socket地址同时获取以字符串表示的主机名(
内部使用的是gethostbyaddr函数
)和服务名(
内部使用的是getservbyportr函数
)。
#include <netdb.h>
//成功时返回0,失败时返回错误码
int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);
参数解释:
sockaddr:要操作的socket地址
addrlen:要操作的socket地址的长度
host:指向存有主机名的缓存
hostlen:存有主机名的缓存的长度
serv:指向存有服务名的缓存
servlen:存有服务名的缓存的长度
flags:控制getnameinfo行为
下图表是flags参数的的取值及对应的含义:
(5)gai_strerror
gai_strerror()函数能将错误码转换成相应的字符串形式
。
#include <netdb.h>
const char* gai_strerror(int error);
参数解释:
error:系统调用返回的错误码
5.10 注意点
(1)TCP客户端套接字的IP地址为主句IP,port由操作系统随机分配,都在调用connect时分配。
(2)服务端有两个socket:一个是用于接收连接请求的socket,socket()调用时创建;一个是用来传输数据的socket,accept()调用时创建。connect与listen相对应,每当有一个连接请求connect,listen的队列中就会加入一个连接;每当服务端accept一个请求,就从listen队列的队首取出一个连接。
(3)TCP和UDP创建socket时可以使用相同的端口号。但是多个TCP(或UDP)类型的socket不能使用相同的端口号。
从socket()和bind()两个函数可以看出,一个socket通过协议、IP和Port来区分的,并且IP数据报头部还有一个协议字段来区分该数据是TCP还是UDP类型(6表示TCP,17表示UDP)。