1. Socket基础概念:
1.1:形象类比:
Socket和电话网络的概念可以做一个很好的类比:
Linux 编程中所说的socket就如同一个端点,类比到电话网中,它就如同一个电话机。 而Socket地址就如同电话网中的电话号码。。
1.2:socket地址和地址格式:
Berkeley小组在构思BSD socket时, TCP/IP协议也正在发展,同时,其它很多协议也正在研究,产生和应用中。所以socket允许使用多种协议,而非仅使用TCP/IP协议。所以,需要socket能够适应多种协议的地址格式。
1.2.1: 创建socket endpoint 时需指定的信息:
同上原因,socket endpoint在创建时(使用socket(2)),也需要指明是何种协议. 在Socket的早期设计中,设计者考虑以此种方式细分协议。
A:指出协议族(domain)
B: socket类型(type)
C: 协议。(protocol)
这其实就是socket(2)系统调用的三个参数。
int socket(int domain, int type, int protocol);
具体信息见2.
1.2.2: 地址格式:
每一种网络通讯协议都对网路地址格式作了明确的规定。大多数网络格式并不相同,这就要求Socket的的构思者能够找到一个合理的方式来处理这个问题。
BSD Socket作如下处理:
地址结构体中,
第一项:地址族。
后面的内容根据地址族的不同而不同。当时ANSI C标准还未被采纳,所以没有使用(void*) 数据类型来接收结构化的地址。BSD的解决方案是:定义了一个通用的地址结构如下:
struct sockaddr {
sa_family_t sa_family; //地址族,
2字节
char sa_data[14]; // 地址数据
}
(man 2 bind 可见)
具体到某种地址租,则地址格式会有很大不同,但与sockaddr类似,例如:
TCP/IP V4协议(man 7 ip):
struct sockaddr_in {
sa_family_t sin_family; // address family: AF_INET
2字节
in_port_t sin_port; // port in network byte order
2字节
struct in_addr sin_addr; // internet address
4字节
};
struct in_addr {
uint32_t s_addr;
};
UNIX本地协议(man 7 unix)
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; // AF_UNIX 2字节
char sun_path[UNIX_PATH_MAX]; // pathname 108字节
};
TCP/IP V6(man ipv6):
struct sockaddr_in6 {
sa_family_t sin6_family;
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};
struct in6_addr {
unsigned char s6_addr[16];
};
其它如X.25, IRDA, Bluetooth 也各不相同。
2. 协议族和地址族详解:
前面在不同的地方分别使用了协议族(PF)和地址族(AF)这两个关键词。很多人认为他们是相同的,这在很多情况下都是对的,但并不代表在任何情况下都是正确的。
BSD socket的设计者考虑到在指定socket时,可能还需要进一步的细分类别:
A: 某个协议族中有一个或多个协议。
B: 某个协议中有一个或多个地址格式。
但后来的经验说明:任何一个给定的协议族,都只有一种地址格式。
所以在Linux中,AF_INET和PF_INET相同。AF_UNIX与PF_UNIX也相同。
但这并不意味着未来或者在其它平台上也一定如此:
所以建议是:在socket(2)的第一个参数等明确使用协议族的地方,采用PF_XXXXX
而在socket地址 的地址族sa_family 处,采用AF_XXXX.
具体的来说:协议族有一下一些:
AF_UNIX, AF_LOCAL Local communication
AF_INET IPv4 Internet protocols
AF_INET6 IPv6 Internet protocols
AF_IPX IPX - Novell protocols
AF_NETLINK Kernel user interface device
AF_X25 ITU-T X.25 / ISO-8208 protocol
AF_AX25 Amateur radio AX.25 protocol
AF_ATMPVC Access to raw ATM PVCs
AF_APPLETALK Appletalk
AF_PACKET Low level packet interface
AF_IRDA
AF_BLUETOOTH
IRDA和Bluetooth都在各自的模块头文件中包含。
3. 连接类型:
socket(2)的第二个参数type. 就是用来指出此socket endpoint支持的连接类型。
常用的类型有以下几种:
SOCK_STREAM:提供一个顺序确定的,可靠的,双向基于连接的socket endpoint. 支持带外数据(out-of-band data)
SOCK_DGRAM : 提供一个数据报(非连接,不可靠)
SOCK_SEQPACKET:
提供一个顺序确定的,可靠的,双向基于连接的socket endpoint. 与SOCK_STREAM不同的是,它保留消息边界。(表明发送两个数据包,只能分两次读入,稍后详细解释)
SOCK_RAW:提供Raw 数据 socket endpoint.
SOCK_RDM : 提供可靠的数据报socket endpoint. (顺序不确保)
还有几个类型已经被放弃使用。
4. 协议:
系统调用socket(2) 的参数三。指出使用何种协议。
BSD在设计socket时,有如下想定:一个协议族中,可以有一个或多个socket类型,一个socket类型,又可以有多个协议。 但在TCP/IP下,通常协议族类型和socket类型确定后,只有一种协议可以使用。所以大家常简单的把socket(2)的参数三置0。Linux Kernel就会自动选择正确的协议。
但在其它协议族中,某socket类型并不只对应一种协议(例如稍后介绍的Bluetooth)。所以,socket(2)的参数三protocol还是有意义的。
5. socket endpoint实际创建:
如上所述,建立一个socket endpoint,需要首先明确三点:
协议族,socket类型,协议。
现在我们就不同的排列组合看看不同协议族,不同socket类型如何创建socket endpoint. (并不是所有排列组合都可用,例如:一些协议族并不支持某种socket类型)
TCP/IP v4:
5.1:创建一个可靠的,基于连接的socket endpoint.(用于TCP连接)
iSockt_client = socket(
PF_INET, SOCK_STREAM, 0);
if(iSockt_client == -1)
{
perror("socket()");
return -1;
}
可以看到:
协议族使用:PF_INET.
socket类型:SOCK_STREAM(会创建一个tcp socket, 可连接)
SOCK_STREAM 这个类型还有一个特点:数据流中没有分界线,也没有边界,没有记录的长度或块大小,在接收端也不存在分组的概念,接收端获取的所有数据都返回到调用者的缓冲区中。这个特点就是它与
SOCK_SEQPACKET最大的区别。
本地endpoint 通过write(2)系统调用向远端发送了两组数据,一组20字节,一组10字节。
远端通过read(2)读取数据,接收缓冲区设置256。则它一次性读取30个字节,完全不清楚对端是通过两个write(2)动作发送的数据。
5.2:创建一个不基于连接(udp)的连接:
iSockt_client = socket(
PF_INET, SOCK_DGRAM, 0);
if(iSockt_client == -1)
{
perror("socket()");
return -1;
}
与tcp不用,udp连接不能保证数据正确到达,也无法保证顺序。数据报有大小限制。如果超出限制,某些路由器和结点无法传送。可以传送给多个IP。
UNIX
5.3:创建本地基于连接和非连接的socket endpoint
基于本地的连接,通常用作父子进程间通讯,因为在本机,所以哪怕使用不可靠连接,丢失包的概率也很低。而又因为SOCK_DGRAM类型的数据保留边界信息。所以
PF_LOCAL, SOCK_DGRAM
这个组合和实用
。
iSockt_client = socket(
PF_LOCAL, SOCK_DGRAM, 0);
if(iSockt_client == -1)
{
perror("socket()");
return -1;
}
iSockt_client = socket(
PF_LOCAL, SOCK_STREAM, 0);
if(iSockt_client == -1)
{
perror("socket()");
return -1;
}
Bluetooth
5.4: 创建Bluetooth RAW连接,protocol为hci:
ctl = socket(
AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI)
这里可以看到AF_BLUETOOTH协议族,使用SOCK_RAW类型,会有多种协议,所以参数三 protocol要明确支出何种协议。
5.5: 创建Bluetooth l2cap连接:
sk = socket(
PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP)
类似的,Bluetooth协议族中,SEQPACKET类型的协议种类众多,所以要明确指出选用l2cap protocol.
关于
SOCK_SEQPACKET:在很多方面和
SOCK_STREAM
类似。 可靠,数据顺序得到保证。但它记录了数据边界。所以发送端写入几次,接收端就需要至少接收几次。便于程序处理。
Linux下使用Socket编程,支持多种协议族,象IPv4, IPv6, Local, X.25, IRDA, IPX, Bluetooth等。但我们最常用的还是IPv4。 下面集中关注一下IPv4的地址和转换。
0. 基础知识:
0.1: 网络字节序:
对于多字节数据,不同的CPU有着不同的组织方式,最基本的两种方式如下:
little-endian(小端)和Big-endian(大端)字节序。
Little-endian(小端):将低序字节存储在起始位置的方法。
Big-endian(大端):将高序自己存储在起始位置的方法。
Intel,ARM等均采用little-endian. 但Motorola和网络数据均采用Big-Endian方式。
所以当某个参数或数据采用网络字序时,我们需要进行转换,当然转换的方向有两个:
主机字节序到网络字节序。 网络字节序到主机字节序。
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
h:表示host。主机。
n:表示Net。 网络:
s: short. 16位数。
l: long. 32位数。
0.2. Internel IP地址和分类:
一个IPv4地址 是由4个被小数点隔开的十进制数组成的。我们把它称之为点分十进制表示法。每个数由一个字节保存。所以取值范围是:0-255.
0.2.1: Internel IP地址分类:
我们常听到A类IP地址,B类IP地址等,它们具体指什么呢?下面分析之。
一个Internel Ip地址由两部分组成,网络地址和主机地址。
网路地址指定主机所在网络,而主机地址用来区分同一网络内不同主机。
一个IP地址由4个字节组成如:xxx.xxx.xxx.xxx。 那哪部分是网络地址,哪部分是主机地址呢? 其实并不是固定的。 而正式由于网络地址和主机地址所占位数的不同,而决定了地址的分类(A类,B类,C类)
A类地址:
第一个字节为:网路地址(
最高位固定为0,所以网络地址7bit)。主机地址3个字节(24bit)
最低:0.0.0.0 最高:127.255.255.255
B类地址:
前两个字节中包含网络地址(
最高两位固定为:10,所以网络地址14bit). 主机地址2字节(16bit)
最低:128.0.0.0 最高:191.255.255.255
C类地址:
前三字节中放置网络地址(
最高三位固定为:110。所以网络地址21bit). 主机地址1字节(16bit)
之前一直听到中国没有A类地址,但在访问很多网站时,发现其IP地址为A 类,如CSDN:114.112.73.194
感觉准确的说法是:中国没有得到一个完整的A类地址段。即没有一个首地址为0-127的2的24次方的网段由中国管理。
0.2.2: 网络掩码:
网络掩码用于把网络地址从IP地址中提取出来。
IP地址与网络掩码按位相与。则得到网络地址。
1. IP地址的转换:
为了方便的在点分十进制表示的IP地址和网络序IP地址之间转换,系统提供了一系列转换函数。现介绍如下:
1.1: 从点分十进制到网络序:
1.1.1:int
inet_aton(const char *cp, struct in_addr *inp);
把参数一从 IPv4 numbers-and-dots格式转换到参数二的in_addr结构体指针中去(in network byte order).
in_addr_t
inet_addr(const char *cp); 也是同样的作用,但不再推荐使用。(如果出错,errno没有被写入)
char sIp[16] = {0};
strcpy(sIp, "192.168.0.25");
struct sockaddr_in sock_addr;
memset((void*)&sock_addr, 0 , sizeof(struct sockaddr_in));
iRet = inet_aton(sIp, &sock_addr.sin_addr);
if(iRet = 0)
{
return -1;
}
注意: 因为已经网络序,所以无需再使用htonl()转换。另外,出错时不会建立errno.所以不用去检测。
1.2: 从点分十进制到主机序:
in_addr_t
inet_network(const char *cp);
返回值为:in_addr_t类型,见附录。
host_addr = inet_network(sIp);
if(host_addr == -1)
{
return -1;
}
1.3: 从主机序到网络序:
呵呵,htonl()
1.4: 从网络序到点分十进制(字符串)
char *
inet_ntoa(struct in_addr in);
给定网络序的IP地址,返回字符串(
点分十进制)。 这个返回值是个静态字符串Buffer。 下次调用前有效。
其它还有三个有用的函数:
in_addr_t inet_lnaof(struct in_addr in);
从IP地址中得到本地地址部分(local network address). 主机序
in_addr_t inet_netof(struct in_addr in);
从IP地址中得到网络地址部分(network number)主机序。返回值右端对其,主机部分被移除了。
struct in_addr inet_makeaddr(int net, int host);
从网路地址和主机地址(都是主机序)得到IP地址(网络序)
2. IPv4 网络地址的生成:
有了上一节中对IPv4地址格式的讨论,又有了这一节中对网络序,地址转换的讨论。则我们要生成一个IPv4网络地址结构体会非常容易。
首先看IPv4地址结构体:
man 7 ip
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
struct in_addr {
uint32_t s_addr;
};
逐项分析:
sin_family:对IPv4来说,这一项一定是AF_INET。如果此项不设置,会产生EINVAL错误。
sin_port:用网路序(network byte order)存放端口号。一个16bit无符号数(见附录),所以用的是htons(),ntohs();
小于1024的端口是系统保留的,供一些特殊服务使用。例如:21-FTP, 22-ssh, 23-telnet. 编程时最好不要在不了解得情况下使用这些端口,否则会导致一些系统功能无法使用。
sin_addr: 是个结构体,这个结构体中包含32bit无符号数。使用网络序存放IP地址。它要么分配为:INADDR_*(例如: INADDR_ANY) 或者通过 inet_aton(3)给定值。
初始化一个通配的Internet地址:
当一台主机有多个网卡或者多个IP地址时,我们可以建立一个通配的地址。它表示这个Socket 对多有IP都有效。
当希望内核自动分配port时,可以指定port =0.
struct sockaddr_in addr_client;
memset((void*)&
addr_client, 0, (size_t)sizeof(struct sockaddr_in));
addr_client.sin_family = AF_INET;
addr_client.sin_port = htons(0);
addr_client.sin_addr.s_addr=htonl(INADDR_ANY);
INADDR_ANY为32位无符号0. 见附录。
初始化一个指定的Internet地址:
short port = 9001;
struct sockaddr_in addr_server;
char server_ip[16] = {0};
strcpy(server_ip, "10.0.0.2");
memset((void*)&addr_server, 0, (size_t)sizeof(struct sockaddr_in));
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(port);
inet_aton(server_ip, (struct in_addr *)&addr_server.sin_addr);
附录:
typedef uint32_t
in_addr_t;
struct
in_addr {
uint32_t s_addr;
};
typedef uint16_t
in_port_t;
#define INADDR_ANY ((in_addr_t) 0x00000000)
0. 基础知识:
0.1: 为socket endpoint 和socket地址关系:
当一个socket被系统调用socket(2)创建时,它还处于无名状态。 在第一节的形象类比中说过,socket endpoint就如同电话机,而socket地址就如同电话号码。无名socket就像电话机没有号码一样。 无名状态的socket endpoint并非无法使用,例如采用socketpair(2)创建的成对socket endpoint. 它就不需要绑定,因为所产生的两个socket endpoint就如同苏美总统专线一样,只能和一个特定的对方通话,所以不需要任何号码。
但同样的,如果一个socket(2)所创建的socket endpoint没有号码,则别人就没法找到它,也就没法给请求和它连接和发送信息。所以,我们需要把一个socket endpoint和socket 地址绑定(bind(2))起来。
0.2:基于连接的两个Socket Endpoint,在其中一个bind(2) socket地址后,另一个就可以像它发起连接请求,我们把发起请求方叫做client, 接收请求方叫做server. 在server监听到连接请求后,可以接受请求而建立一个新的连接。这样,他们就可以传送数据。任意一方都可以随时关闭这个连接。
1. Server 端实例:
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char** argv)
{
int iSocket_server = 0;
struct sockaddr_in addr_server;
struct sockaddr_in addr_peer;
uint16_t port = 9001;
int iRet = 0;
int iSocket_Data = 0;
char buffer[1024] = {0};
int iData_length = 0;
char Message[12] = {0};
unsigned int len = 0;
// 1. 创建socket
iSocket_server = socket(PF_INET, SOCK_STREAM, 0);
if(iSocket_server == -1)
{
perror("socket()");
return -1;
}
// 2. 绑定
memset(&addr_server, 0, sizeof(struct sockaddr_in));
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(port);
addr_server.sin_addr.s_addr = htonl(INADDR_ANY);
iRet = bind(iSocket_server, (struct sockaddr *)&addr_server, sizeof(struct sockaddr_in));
if(iRet == -1)
{
perror("bind()");
return -1;
}
// 3. listen
iRet = listen(iSocket_server, 10);
if(iRet == -1)
{
perror("listen()");
return -1;
}
for(;;)
{
printf("\n################Waiting for connect#######################\n");
memset(&addr_peer, 0, sizeof(struct sockaddr_in));
// 4. accept
len = sizeof(struct sockaddr_in);
iSocket_Data = accept(iSocket_server, (struct sockaddr *)&addr_peer, &len);
if(iSocket_Data == -1)
{
perror("accept()");
continue;
}
printf("\nAccept connect from : [%s:%d]\n", inet_ntoa(addr_peer.sin_addr), ntohs(addr_peer.sin_port));
//sleep(5);
iData_length = read(iSocket_Data, buffer, 1024);
if(iData_length == -1)
{
perror("read()");
close(iSocket_Data);
continue;
}
printf("\nReceive Message:[%s]. Data length:[%d]\n", buffer, iData_length);
memset(buffer, 0, 1024);
iData_length = read(iSocket_Data, buffer, 1024);
if(iData_length == -1)
{
perror("read()");
close(iSocket_Data);
continue;
}
printf("\nReceive Message:[%s]. Data length:[%d]\n", buffer, iData_length);
strcpy(Message, "bye");
//sleep(5);
iData_length = write(iSocket_Data, Message, 3);
if(iData_length == -1)
{
perror("write()");
close(iSocket_Data);
continue;
}
printf("\nsend Message:[%d]\n", iData_length);
close(iSocket_Data);
}
return 0;
}
分析如下:
A. 创建socket endpoint:
iSocket_server = socket(PF_INET, SOCK_STREAM, 0);
协议族是IPv4----PF_INET
类型是stream.
B. 为socket绑定地址:
没有绑定地址的socket endpoint就如同没有号码的电话机,别人无法申请连接。所以需要先行绑定。
memset(&addr_server, 0, sizeof(struct sockaddr_in));
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(port);
//指定端口
addr_server.sin_addr.s_addr = htonl(INADDR_ANY);
//通配地址,当前机器所有IP都监控。
iRet = bind(iSocket_server, (struct sockaddr *)&addr_server, sizeof(struct sockaddr_in));
bind(2)所指定的socket 当前必须处于无名状态(还未指定地址)。 不能为一个已有地址的socket 重新bind一个新的地址。
bind(2)的另一个意义在于:它利用给socket endpoint指定socket 地址的方式限定用于通讯的接口。
例如:某主机有多个IP地址:
127.0.0.1
10.0.0.2
192.168.0.2
当利用bind(2)为socket-A绑定了10.0.0.2:9000,则Socket-A只能接收10.0.0.x网段发送给10.0.0.2:9000的信息。
而socket-B绑定了192.168.0.2:19001,则Socke-B只能接收192.168.0.x网段发送给192.168.0.2:19001的信息。
C: listen
listen(2)的作用是:通知Kernel当前程序愿意在指定的socket上监听并接收连接请求。(此socket即成为监听socket)
int listen(int sockfd, int backlog);
listen(2)的参数二值得注意,这个值是指所建立的监听队列的长度。
当有一个连接请求发生时,这个请求被放入监听队列,由accept(2) 处理这个请求,在此期间,有其它请求时,他们被放入监听队列。等待accept()处理。
backlog的取值:不能为负数,也别为0(在某些系统中,0表示不接受连接). 如果连接很繁忙,建议10-20. 如果程序的设计模式使accept()的间隔较大,则backlog这个值应该稍大。
D: accept:
接受
监听socket所接收的连接请求,并创建一个新的连接来处理与对方的数据沟通。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数一:一定是一个监听socket(listen(2)把一个socket endpoint声明为
监听socket)
参数二:申请连接的对端socket地址。
参数三:输入输出参数。输入时为地址最大值,输出时为地址实际长度。
返回值:所创建的一个新socket endpoint. 它与申请连接的对端socket endpoint已经连接起来了。数据交换其实是通过这个新socket完成的,而非监听socket.
E: 数据交换:
read(2), write(2)
F:close 连接socket:
int close(int fd);
关闭accept(2)新创建的socket.
G: close 监听socket:
关闭监听socket
2. Client实例:
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char** argv)
{
int iSocket_Client = 0;
struct sockaddr_in addr_server;
uint16_t port = 9001;
char server_ip[16] = "10.0.0.2";
unsigned int addr_len = 0;
int iRet = 0;
char Message[108] = {0}; //
unsigned int uiLen_Data = 0;
char buffer[1024] = {0};
iSocket_Client = socket(PF_INET, SOCK_STREAM, 0);
if(iSocket_Client == -1)
{
perror("socket()");
return -1;
}
int on = 1;
setsockopt( iSocket_Client, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
memset(&addr_server, 0 , sizeof(struct sockaddr_in));
addr_server.sin_family = AF_INET;
inet_aton(server_ip, (struct in_addr *)(&addr_server.sin_addr));
addr_server.sin_port = htons(port);
addr_len = sizeof(struct sockaddr_in );
iRet = connect(iSocket_Client, (struct sockaddr *)&addr_server, addr_len);
if(iRet == -1)
{
perror("connect()");
return -1;
}
printf("\n############################Connect Server Success##############################\n");
#if 1
strcpy(Message, "Client Send Message to Server.");
uiLen_Data = write(iSocket_Client, Message, strlen(Message));
if(uiLen_Data == -1)
{
perror("write()");
return -1;
}
printf("\nSend [%d] Data Success.[%s]\n", uiLen_Data, Message);
strcat(Message, "++");
uiLen_Data = write(iSocket_Client, Message, strlen(Message));
if(uiLen_Data == -1)
{
perror("write()");
return -1;
}
printf("\nSend [%d] Data Success.[%s]\n", uiLen_Data, Message);
sleep(1);
#endif
#if 0
uiLen_Data = read(iSocket_Client, buffer, 1024);
if(uiLen_Data == -1)
{
perror("read()");
return -1;
}
printf("\nReceive Message:[%s]\n", buffer);
#endif
for(int i = 0; i < 10; i++)
{
printf(".");
fflush(stdout);
sleep(1);
}
close(iSocket_Client);
return 0;
}
分析如下:
连接请求:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数二:想要连接的socket地址。
Pv4 非连接的通讯,就是在通信前不需要建立连接,它的优点如下:
A: 更加简单。不需要建立连接。
B: 更有弹性,每一次的消息发送都可以定向到不同的接收者。
C: 更高效。因为不需要建立和拆除连接,所以就避免了大量网络开销。
D: 具备广播能力。可以将一个消息同时向多个接收者发送。
一个硬币总有两面,UDP的缺点则是:
A: 通信过程不可靠。
B: 多数据报的无序性。
C: 消息的尺寸有限制。
通讯不可靠的原因有多种:在数据包的传送中, 某个路由器在转发过程中发现数据校验错误,路由器或接收主机无法为UDP提供足够缓存空间,都会导致数据丢失。UDP数据传送的距离越长, 发生丢失的概率就越大。
多数据报的无序性:UDP使用IP协议进行数据报的传输,而根据当前的网络状况,每一个IP分组的路由都有可能不同,这有可能导致后发的数据却先到了。
消息尺寸:
虽然理论上UDP的数据报最大尺寸可以略小于64K,但实际上很多UNIX主机之提供接近32K甚至8K 的最大尺寸. 为了保险起见,UDP消息尺寸为512B 或者小于512字节比较合适。
1. Server 端实例:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char** argv)
{
int iSocket_Server = 0;
int iRet = 0;
struct sockaddr_in addr_server;
struct sockaddr_in addr_peer;
uint16_t port = 9001;
char server_ip[16] = {0};
socklen_t addr_len = 0;
char buffer[1024] = {0};
unsigned int uiPeer_addr_len = 0;
iSocket_Server = socket(PF_INET, SOCK_DGRAM, 0);
if(iSocket_Server == -1)
{
perror("socket()");
return -1;
}
memset((void*)&addr_server, 0, sizeof(struct sockaddr_in));
strcpy(server_ip, "10.0.0.2");
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(port);
inet_aton(server_ip, &addr_server.sin_addr);
addr_len = sizeof(struct sockaddr);
iRet = bind(iSocket_Server, (struct sockaddr *)&addr_server, addr_len);
if(iRet == -1)
{
perror("bind()");
return -1;
}
for(;;)
{
uiPeer_addr_len = sizeof(struct sockaddr_in);
printf("\nWaiting for Message from client.....\n");
iRet = recvfrom(iSocket_Server, buffer, sizeof(buffer), 0, (struct sockaddr *)&addr_peer, &uiPeer_addr_len);
if(iRet == -1)
{
perror("recvfrom()");
continue;
}
printf("Get [%d] data from: [%s:%d]. [%s]", iRet, inet_ntoa(addr_peer.sin_addr), ntohs(addr_peer.sin_port), buffer);
}
close(iSocket_Server);
return 0;
}
分析如下:
建立Socket后,为它bind(2)地址,利用recvfrom(2) 获取任何人向这个地址发送的数据报。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
其中参数src_addr中会填充发送此数据的socket地址。
它使我们在接受到数据的同时,也得到这个数据发送者的地址。
2. Client实例
:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char** argv)
{
int iSocket_client = 0;
struct sockaddr_in addr_server;
uint16_t port = 9001;
char server_ip[16] = {0};
char Message[256] = {0};
int iRet = 0;
socklen_t addr_len = 0;
iSocket_client = socket(PF_INET, SOCK_DGRAM, 0);
if(iSocket_client == -1)
{
perror("socket()");
return -1;
}
strcpy(Message, "Hello Server. This Message form Client!");
memset((void*)&addr_server, 0, sizeof(struct sockaddr_in));
strcpy(server_ip, "10.0.0.2");
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(port);
inet_aton(server_ip, &addr_server.sin_addr);
addr_len = sizeof(struct sockaddr);
iRet = sendto(iSocket_client, Message, strlen(Message), 0, (struct sockaddr *)&addr_server, sizeof(struct sockaddr_in));
if(iRet == -1)
{
perror("sendto()");
return -1;
}
printf("\nSend Message Success\n");
close(iSocket_client);
return 0;
}
利用sendto(2)向指定的socket地址发送数据报。它只管发送,对端是否接收,不在考虑范围内。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数dest_addr为此次发送的目标socket地址。
0. 基础知识:
之前在《
Linux Socket详解<三> IPv4 tcp实例
》中谈到过bind(2)的作用。
作用1显然是给socket endpoint指定一个socket地址,否则别人无法连接或者给它发送数据。
但作用2就比较隐蔽,当一个主机有多个IP地址时,通过给socket endpoint绑定socket地址,可以限定用于通信的接口。如绑定了10.0.0.2:9000. 则发送给同一台主机的另一个IP 192.168.0.2:19001的连接请求就不会去理会。
如何才能在任意一个接口上接收连接,或者在任何一个接口上发送连接请求呢?可以使用通配Socket地址--
INADDR_ANY.
1. INADDR_ANY 详解:
linux/in.h:#define INADDR_ANY ((unsigned long int) 0x00000000)
它代表的是主机上所有IP。
作为Server端:
当某台设备上有多个IP时,(多个网卡,或一个网卡上不止一个IP地址),如果想要侦听所有IP的某个特定端口X,就可以使用INADDR_ANY. 将其bind(2)到socket endpoint上。
这时,相当于向Kernel宣布:程序要侦听的是这台设备上所有IP的端口X。不管哪个网卡,哪个IP地址的这个端口X的所有数据,均归我处理。
作为Client端:
作为Client端,当有多个IP地址时,也可以采用bind(2) INADDR_ANY的方式显示对所有IP都有效。在不同的阶段,它会作绑定动作。
对TCP Client来说,在connect(2)时,就会将其绑定到某一具体IP地址。选择哪个IP地址的依据是该IP地址所在的子网到目标地址是可达的(Rechable)。这时,通过getsockname()系统调用就可以得到具体采用那个IP地址。
对UDP Client来说又有不同,它是在sendto(2)才绑定某个具体IP地址。选择依据和TCP一致。
2. Client不做bind(2)动作的效果:
不管在UDP还是在TCP协议中,Client端均可以不做bind(2)动作。其实这种情况就等同于使用:INADDR_ANY+ port=0的效果。
Port =0. 即告知Kernel 为socket自动分配未使用的Port的意思。
之前讨论过基于连接的TCP程序,那个程序相当简单。但有个明显的不足:服务器一次只能处理一个Client。只有这个Client的工作处理完成后,才去处理下一个Client。若其中一个Client处理时间很久,则其它Client只能等待。这明显是很难满足并发client的。
有多种方式能解决这个问题: 如accept(2)后立刻使用fork(2)采用新进程去服务client. 或者使用多thread.
但还有个方法更加有效,利用select(2)或者poll(2)实现单进程服务器模式。
1. select(2)函数介绍:
select(2)使我们在SVR4和4.3+BSD之下可以执行I/O多路转接。
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
select(2)函数告知Kernel: a.我们所关心的描述符。b.对于每个描述符,我们所关心的条件(是否可读,是否可写,是否有异常)。 c. 希望等待的时间。
select(2)返回时,Kernel会负责告诉我们:
a. 已经准备好的描述符数量。
b. 哪一个描述符准备好读,写,或异常条件。
当参数5 timeout为NULL时,select(2)会永远等待,直到所指定的描述符中的一个已经准备好或捕捉到一个信号。
参数2,3,4:readfds, writefds, exceptfds是指向描述符集合的指针。它们分别放置了我们设定的可读,可写,处于异常条件的各描述符。
他们可以为NULL,表明对相应的条件并不关心。
参数1:nfds, 最大的fd+1. 指三个描述符集中找到最高的描述符编号值,然后加1。 有人喜欢把这个值直接设置为:FD_SETSIZE. (sys/types.h中设置的常数,表明最大的描述符数)。但这并不是好办法。因为Kernel会根据这个值在一定范围内查询。如果值太大,效率会降低。
#define __FD_SETSIZE 1024
#define FD_SETSIZE __FD_SETSIZE
返回值:
-1:以上描述符都没有准备好时就捕捉到一个信号。
0: 以上描述符都没有准备好,但Timeout。
正数:已经准备好的描述符数量。此时,描述符集中打开的对应位就是已经准备好的描述符位。
所谓准备好:
对于读集合(readfds),表明描述符的read不会阻塞,则此描述符是准备好的。
对于写集合(writefds),描述符write时不会阻塞,则此描述符是准备好的。
如果一个描述符碰上了文件结束,则select(2)认为此描述符是可读得。然后调用read(2), 它返回0,这是UNIX指示到达文件结尾处的方法。
文件描述符集合的使用:
select(2)中的2,3,4三个参数readfds, writefds, exceptfds都是指向描述符集的指针(fd_set*)。这个类型对用户并不透明,需要使用宏进行操作。
void FD_ZERO(fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
初始化文件描述符集,其中所有位均归零。
void FD_SET(int fd, fd_set *set);
设置文件描述符集中对应fd的位为1。也就是把fd加入了文件描述符集合。
void FD_CLR(int fd, fd_set *set);
把fd移除文件描述符集合。
int FD_ISSET(int fd, fd_set *set);
查看fd是否存在于文件描述符集合set中。
2. 利用select(2)实现多客户TCP Server实例:
这个程序用来接收客户端的TCP连接请求,并把它们传送的文件存储起来。
//==================================
//system header file
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//==================================
//local struct
typedef struct socket_handle
{
int Socket_Handle; // socket
int used; // 0. No Used. 1. used
FILE* fp; // file handle
} socket_handle;
//==================================
//local define
// 同时可以接受的链接数
#define CONNECT_DEVICE_MAX 16
// 正在连接的设备的信息. 包括Socket,文件描述符
static socket_handle gConnect_Info[CONNECT_DEVICE_MAX]; // connect list
static int gConnect_Num = 0; // 正在连接的设备数
static int gData_Num = 0; // 发送过来的数据总数
int main(int argc, char** argv)
{
int iSocket_Server = 0;
int iSocket_Tranf = 0;
struct sockaddr_in addr_server;
struct sockaddr_in addr_peer;
int len_inet = 0;
short port = 9001;
int iRet = 0;
char buf[2048] = {0};
FILE* fp;
int iSize = 0;
fd_set rx_set;
fd_set backup_set;
int nfds = 0;
//static int index = 0;
int iRet_Select = 0;
int iFind_Device = 0;
char FileName[1024] = {0};
struct stat tStat;
FD_ZERO(&rx_set);
FD_ZERO(&backup_set);
memset(gConnect_Info, 0, (sizeof(socket_handle)*CONNECT_DEVICE_MAX));
// 1. socket
iSocket_Server = socket(AF_INET, SOCK_STREAM, 0);
if(iSocket_Server == -1)
{
perror("socket()");
return -1;
}
// 2. bind()
memset((void*)&addr_server, 0, (size_t)sizeof(struct sockaddr_in));
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(port);
addr_server.sin_addr.s_addr=ntohl(INADDR_ANY);
iRet = bind(iSocket_Server, (const struct sockaddr *)&addr_server, sizeof(struct sockaddr_in));
if(iRet == -1)
{
perror("bind()");
return -1;
}
// 3. listen()
iRet = listen(iSocket_Server, 10);
if(iRet == -1)
{
perror("listen()");
return -1;
}
FD_SET(iSocket_Server, &backup_set);
nfds = iSocket_Server + 1;
// 4. accept()
len_inet = sizeof(struct sockaddr_in);
//Main Loop
for(;;)
{
// copy backup 描述符集 to rx_set
FD_ZERO(&rx_set);
for(int i = 0; i < nfds; i++)
{
if(FD_ISSET(i, &backup_set))
{
FD_SET(i, &rx_set);
#ifdef _DEBUG
printf("select List: [%d]\n", i);
#endif
//if (-1 == fstat(i, &tStat))
//{
// printf("fstat %d error:%s", i, strerror(errno));
// getchar();
// }
}
}
#ifdef _DEBUG
printf("nfds:[%d]\n", nfds);
#endif
iRet_Select = select(nfds, &rx_set, NULL, NULL, NULL);
// error.
if(iRet_Select <= 0)
{
perror("select()");
//return 0; // for test
continue;
}
else
{
// 如果有连接申请,则创建新连接(accept). 并把信息存储起来
if(FD_ISSET(iSocket_Server, &rx_set))
{
iSocket_Tranf = accept(iSocket_Server, (struct sockaddr *)&addr_peer, (socklen_t*)&len_inet);
if(iSocket_Server == -1)
{
perror("accept()");
return -1;
}
printf("\naccept connect from:[%s:%d]\n", inet_ntoa(addr_peer.sin_addr), ntohs(addr_peer.sin_port));
gConnect_Num++; //连接数
if(gConnect_Num > CONNECT_DEVICE_MAX)
{
close(iSocket_Tranf);
continue;
}
iFind_Device = 0;
for(int i = 0; i < CONNECT_DEVICE_MAX; i++)
{
if(gConnect_Info[i].used == 0) // not used
{
gConnect_Info[i].used = 1;
gConnect_Info[i].Socket_Handle = iSocket_Tranf;
iFind_Device = 1; // save
sprintf(FileName, "Zienon_%d.dat", gData_Num);
gData_Num++;
fp = fopen(FileName, "w+");
if(fp == NULL)
{
perror("fopen()");
gConnect_Info[i].used = 0;
iFind_Device = 0; // No Found.
break;
}
gConnect_Info[i].fp = fp;
if((iSocket_Tranf + 1) > nfds )
{
nfds = iSocket_Tranf + 1;
}
break;
}
} //for(int i = 0; i < CONNECT_DEVICE_MAX; i++)
if(iFind_Device)
{
// 将新建连接socket加入Select监控描述符集中
FD_SET(iSocket_Tranf, &backup_set);
}
} //if(FD_ISSET(iSocket_Server, &rx_set))
// 检查所有Socket情况
for(int i = 0; i < CONNECT_DEVICE_MAX; i++)
{
if(gConnect_Info[i].used == 1)
{
if(FD_ISSET(gConnect_Info[i].Socket_Handle, &rx_set))
{
if(gConnect_Info[i].Socket_Handle == iSocket_Server)
{
continue;
}
if((iRet = read(gConnect_Info[i].Socket_Handle, buf, 2048)) > 0 )
{
iSize = fwrite(buf, 1, iRet, gConnect_Info[i].fp);
//printf("\nWrite [%d] byte\n", iSize);
if(iSize != iRet)
{
printf("\nSave File error.\n");
break;
}
bzero(buf, sizeof(buf));
}
else if(iRet == 0) // peer socket close
{
//close file
fclose(gConnect_Info[i].fp);
printf("Close [%d] Success\n", gConnect_Info[i].fp);
// close socket
//sleep(1);
close(gConnect_Info[i].Socket_Handle);
printf("Close [%d] connect\n", gConnect_Info[i].Socket_Handle);
//Remote it from 文件描述符集合
FD_CLR(gConnect_Info[i].Socket_Handle, &backup_set);
printf("remote [%d] from backup_set\n", gConnect_Info[i].Socket_Handle);
gConnect_Info[i].used = 0;
gConnect_Info[i].Socket_Handle = 0;
gConnect_Info[i].fp = NULL;
gConnect_Num--;
// reset nfds
nfds = iSocket_Server + 1;
for(int j = 0; j < CONNECT_DEVICE_MAX; j++)
{
if(gConnect_Info[j].used == 1)
{
if((gConnect_Info[j].Socket_Handle + 1) > nfds)
{
nfds = gConnect_Info[j].Socket_Handle + 1;
}
}
}
}
else
{
perror("read()");
}
}
}
}
}
}
// 5. close()
close(iSocket_Server);
return 0;
}
代码说明:
1. 建立两个文件描述符集合,一个用来供select(2)使用。另一个用来在有新连接建立或者有连接断掉时更新。
Loop中进行文件描述符集合的copy.
// copy backup 描述符集 to rx_set
FD_ZERO(&rx_set);
for(int i = 0; i < nfds; i++)
{
if(FD_ISSET(i, &backup_set))
{
FD_SET(i, &rx_set);
}
}
2. select(2) 的socket包括:监听socket, 新创建的socket.
2.1把监听socket 加入文件描述符集合。
FD_SET(iSocket_Server, &backup_set);
nfds = iSocket_Server + 1;
2.2: 将新建的连接socket加入文件描述符集合
if((iSocket_Tranf + 1) > nfds )
{
nfds = iSocket_Tranf + 1;
}
// 将新建连接socket加入Select监控描述符集中
FD_SET(iSocket_Tranf, &backup_set);
2.3: select(2) 阻塞式
iRet_Select = select(nfds, &rx_set, NULL, NULL, NULL);
3. 当文件描述符集合中socket 有可读时:
情况分两种:
1. 监听socket可读,说明有新连接请求进来,则调用accept(2)去处理,并把新建socket加入文件描述符集合。
2. 正常Socket可读,说明有数据进来。则读取之。并把数据写入对应文件。
4.当read()返回0时,说明socket TCP连接对端关闭了连接。所以本地也关闭连接,关闭对应数据文件。把socket从文件描述符集合中去除。
之前谈到UDP可以发送广播,这里就讨论下UDP广播。
0. 广播的应用:
在某个子网内,有一台特定设备,但我们不清楚这台设备的地址,可以通过发送UDP广播,设备接收到此特定广播后回复。则可以得到设备的地址。
频繁的广播会加大网络负载,因为它会发送数据到每个地址。
1. 广播地址:
IP地址由两部分组成,NetID(网络地址)和HostID(主机地址)。
网络掩码用于把网络地址从IP地址中提取出来。
IP地址与网络掩码按位相与。则得到网络地址。
HostID每个bit置1. 则此IP即为网络内广播地址。
例如:
IP: 192.168.0.2. MASK: 255.255.255.0
则广播地址:192.168.0.255
IP: 172.16.0.2 MASK: 255.255.0.0
广播地址:172.16.255.255
ifconfig中,会显示广播地址信息:
inet 10.0.0.2 netmask 255.255.255.0
broadcast 10.0.0.255
Sam认为这样说比较合适:
当向广播地址的某个端口发送数据报时,即相当于向本网内每个设备的这个端口发送数据报。
2. 发送广播消息:
要发送广播消息,需要两个基本条件。
a. 建立的socket 激活SO_BROADCAST选项。使socket endpoint成为一个可发送广播的socket.
b. 利用sendto(2) 向广播地址的特定port发送数据报。
也就是说:这个socket 必须能够进行广播。且目的地址必须为一个广播地址。
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char** argv)
{
int iSocket_Send = 0;
int iRet = 0;
static int so_broadcast = 1;
char ip_Server[16] = {0};
struct sockaddr_in addr_server;
struct sockaddr_in addr_broadcast;
uint16_t port = 0;
char Message[36] = "Air raid this is not a drill";
iSocket_Send = socket(PF_INET, SOCK_DGRAM, 0);
if(iSocket_Send == -1)
{
perror("socket()");
return -1;
}
iRet = setsockopt(iSocket_Send, SOL_SOCKET, SO_BROADCAST, &so_broadcast, sizeof(so_broadcast));
if(iRet == -1)
{
perror("setsockopt()");
return -1;
}
memset(&addr_server, 0, sizeof(struct sockaddr_in));
strcpy(ip_Server, "10.0.0.2");
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(port);
inet_aton(ip_Server, (struct in_addr *)&addr_server.sin_addr);
#if 0
iRet = bind(iSocket_Send, (const struct sockaddr *)&addr_server, sizeof(struct sockaddr_in));
if(iRet == -1)
{
perror("bind()");
return -1;
}
#endif
memset(&addr_broadcast, 0, sizeof(struct sockaddr_in));
strcpy(ip_Server, "10.0.0.255");
addr_broadcast.sin_family = AF_INET;
addr_broadcast.sin_port = htons(9090);
inet_aton(ip_Server, (struct in_addr *)&addr_broadcast.sin_addr);
for(;;)
{
sendto(iSocket_Send, Message, strlen(Message), 0, (const struct sockaddr *)&addr_broadcast, sizeof(struct sockaddr_in));
sleep(1);
}
return 0;
}
3. 接收广播消息:
理论上,接收广播消息和接收通常的UDP消息不应该有不同。就是接收指定Port上的数据。
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char** argv)
{
int iSocket_Send = 0;
int iRet = 0;
uint16_t port = 9090;
struct sockaddr_in addr_local;
struct sockaddr_in addr_server;
char buffer[256] = {0};
socklen_t addr_len = 0;
iSocket_Send = socket(PF_INET, SOCK_DGRAM, 0);
if(iSocket_Send == -1)
{
perror("socket()");
return -1;
}
memset(&addr_local, 0, sizeof(struct sockaddr_in));
addr_local.sin_family = AF_INET;
addr_local.sin_port = htons(port);
addr_local.sin_addr.s_addr = htonl(INADDR_ANY);
iRet = bind(iSocket_Send, (const struct sockaddr *)&addr_local, sizeof(struct sockaddr_in));
if(iRet == -1)
{
perror("bind()");
return -1;
}
for(;;)
{
memset(buffer, 0, 256);
memset(&addr_server, 0, sizeof(struct sockaddr_in));
addr_len = sizeof(struct sockaddr_in);
iRet = recvfrom(iSocket_Send, buffer, 256, 0, (struct sockaddr *)&addr_server, &addr_len);
if(iRet == -1)
{
perror("recvfrom()");
continue;
}
printf("\nReceive Message from : [%s:%d].\n", inet_ntoa(addr_server.sin_addr), ntohs(addr_server.sin_port));
printf("\nMessage:[%s]. length:[%d]\n", buffer, iRet);
}
return 0;
}
但看了不少例子,
在接收端,它是把广播地址绑定到接收socket上去。待未来研究。
之前章节的代码段中,类似setsockopt()这样的系统调用。如在UDP广播发送端:
iRet = setsockopt(iSocket_Send, SOL_SOCKET, SO_BROADCAST, &so_broadcast, sizeof(so_broadcast));
或者在tcp client端:
int on = 1;
setsockopt( iSocket_Client, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
当时只是简单的谈到这是为了申明socket是可发送广播。以及这个端口可重用等。这里咱们再就这块内容作更深入的了解。
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);、
这俩系统调用用来得到和设置socket选项。
现大致解析如下:
参数一:sockfd. 创建的socket.
参数二:level. 协议层等级,最常见的是socket API等级,使用SOL_SOCKET。也可以使用SOL_TCP. 关于这个值,参见:getprotoent(3) 这里我们主要讨论SOL_SOCKET, SOL_TCP.
参数三:选项条目。不同的协议层等级对应不同的条目。socket等级的条目在 中定义。可以在
socket(7) 中集中察看。
参数四:optval: 选项缓冲区指针。socket-level的这个缓冲区指向的大都是int. 如果表示bool,则非零表示enable. 0表示disable.
参数五:缓冲区长度(getsockopt()的这个参数是输入/输出参数)
socket-Level的详细信息,可以查看 socket(7)
例1:察看Tcp socket的发送和接收buffer大小。
SO_RCVBUF
SO_SNDBUF
例2:察看某socket是Tcp还是UDP。
SO_TYPE
例3:
设置某个IP:Port 是可以被重复绑定的。
这里很重要,有一些信息需要仔细研究。
在实际Linux Socket编程中,会需要我们得到诸如主机名,主机地址,某个主机的IP地址,和本socket连接的对端socket IP地址等需求。 下面学习之。
1. 获取当前主机名:
int
gethostname(char *name, size_t len);
得到host name. 放入参数一。
char buf[1024]= {0};
iRet = gethostname(buf, 1024);
if(iRet != 0)
{
perror("gethostname()");
return -1;
}
printf("\nHost Name : [%s]\n", buf);
2. 通过主机名获取主机IP地址:
struct hostent *gethostbyname(const char *name);
参数很简单,给定一个主机名, 如: www.sina.com.cn则返回一个指向结构体hostent的指针。其中包含很多信息。同时,这个参数也可以是一个IPv4或者IPv6的地址。
先看看结构体:
struct hostent {
char *h_name; //
official name of host。主机的规范名,正式名
char **h_aliases; //
alias list. 指向别名链表的指针
int h_addrtype; //
host address type . 主机地址类型。IPv4 或IPv6。
int h_length; //
length of address。 地址长度,IPv4为4。IPv6为16
char **h_addr_list; //
list of addresses 指针数组。每个数组中的指针指向一个地址。格式和h_addrtype有关. (in network byte order)
}
#define h_addr h_addr_list[0]
如果函数出错,返回值为NULL。请注意,此函数出错,系统不会置错于errno. 而是h_errno。 所以,perror(), strerror()均不可用。只能使用hstrerror(), herror().
pbaidu = gethostbyname("www.baidu.com");
if(pbaidu != NULL)
{
printf("\n##########################################################\n");
printf("\tGet www.baidu.com addr info\n");
printf("\tofficial name:[%s]", pbaidu->h_name);
for(int i = 0; pbaidu->h_aliases[i] != NULL; i++)
{
printf("\taliases name:[%s]\n", pbaidu->h_aliases[i]);
}
printf("\t host addr type:[%d]\n", pbaidu->h_addrtype);
printf("\t host addr length:[%d]\n", pbaidu->h_length);
for(int i = 0; pbaidu->h_addr_list[i]; i++)
{
printf("\t IP Addr:[%s]\n", inet_ntoa(*(struct in_addr*)pbaidu->h_addr_list[i]));
}
}
else
{
printf("\ngethostbyname() Error. [%s]\n", hstrerror(h_errno));
return -1;
}
gethostbyname()返回的是个静态空间的指针,它有个潜在的问题,
很可能刚要读时值就被其它线程修改。
可以使用:
int gethostbyaddr_r(const void *addr, socklen_t len, int type,
struct hostent *ret, char *buf, size_t buflen,
struct hostent **result, int *h_errnop);
另外,调用这个函数后,它会向DNS Server查询,这段时间不可控(如果网络通讯不好,或者没有配置DNS Server,就会阻塞很久)。需要注意。
3. 通过IP地址得到主机名:
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
通过返回值得到的也是一个指向hostent的指针。但输入参数有所不同。它通过输入IP地址,IP地址长度,地址类型(AF_INET and AF_INET6)来得到hostent中的信息。
最需要注意的参数是参数一:addr. 它的格式依赖于参数三-----type.
如果是AF_INEF(IPv4),则它是:struct in_addr *
struct in_addr addr;
inet_aton("10.0.0.2", &addr);
pNhost = gethostbyaddr((void*)&addr, sizeof(addr),AF_INET);
if(pNhost == NULL)
{
printf("\ngethostbyaddr() Error. [%s]\n", hstrerror(h_errno));
return -1;
}
printf("official name of host:[%s]\n", pNhost->h_name);
printf("alias list\n");
for(int i = 0; pNhost->h_aliases[i]; i++)
{
printf("\t[%s]\n", pNhost->h_aliases[i]);
}
if(pNhost->h_addrtype == AF_INET)
printf("host address type: AF_INET\n");
if(pNhost->h_addrtype == AF_INET6)
printf("host address type: AF_INET6\n");
//printf("host address type:[%d]\n", phost->h_addrtype);
printf("length of address:[%d]\n", pNhost->h_length);
printf("list of addresses\n");
if(pNhost->h_addrtype == AF_INET)
{
for(int i = 0; pNhost->h_addr_list[i]; i++)
{
printf("\t[%s]\n", inet_ntoa(*((struct in_addr*)pNhost->h_addr_list[i])));
}
}
4. 得到某个socket endpoint(已经绑定过socket地址)的地址信息:
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
但如果bind(2)的是INADDR_ANY, 则IP:0.0.0.0
struct sockaddr_in addr_local;
socklen_t length = sizeof(struct sockaddr_in);
iRet = getsockname(iSocket_Server, (struct sockaddr *)&addr_local, &length);
if(iRet == -1)
{
perror("getsockname()");
return -1;
}
printf("\nIP Addr:[%s:%d]\n", inet_ntoa(addr_local.sin_addr), ntohs(addr_local.sin_port));
5.得到连接的对端的IP地址:
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
Linux 下还有一些Socket知识点需要注意,暂时记录,未来填充内容。
1.带外数据:
TCP流中的数据通常是按照顺序发送。但一些紧急数据需要立刻传送,这就用到带外数据。使用带外数据的软件有telnet,ftp等。telnet可以用带外数据发送中断字符,ftp用带外数据中止文件传输。
带外数据发送需要用send(), 其中flags=MSG_OOB来标志这个数据是带外数据。
2.在socket上使用标准I/O:
Linux平台提供stdio - standard input/output library functions。它符合ANSF标准。采用这种标准函数,有利于程序移植。(man 3 stdio)
FILE控制模块用于stdio(3)流的管理。最常看到的做法是:fopen, fread, fwrite, fclose等。
socket如何使用标准I/O呢?可以采用fdopen();
3. 非阻塞socket:
创建一个socket(),缺省模式下它是阻塞的。但有时我们需要一个非阻塞得socket.
fdflags = fcntl(sock_fd, F_GETFL, 0);
if(fcntl(sock_fd, F_SETFL, fdflags | O_NONBLOCK) < 0)
{
close(sock_fd);
exit(1);
}