学习目标
①掌握TCP/IP的基础知识
②掌握嵌入式Linux基础网络编程
③掌握嵌入式Linux高级网络编程
④分析理解Ping源代码
⑤能够独立编写客户端、服务器端的通信程序
⑥能够独立编写NTP实现程序
1 TCP/IP概述
1.1 OSI参考模型及TCP/IP参考模型
OSI参考模型,从上到下共分为7层:应用层、表示层、会话层、传输层、网络层、数据链路层及物理层。
这7个协议因为复杂,没人用,但它仍是许多协议模型的基础,这种分层架构的思想在很多领域都得到了广泛使用。
与此相区别的TCP/IP模型从一开始就遵循简单明确的设计思路,它将TCP/IP的七层模型简化为4层,从而更有利于实现和使用。TCP/IP的参考模型和OSI协议参考模型对应的关系如下图:
下面分别对TCP/IP的4层模型进行简要介绍
①网络接口层:负责将二进制流转换为数据帧,并进行数据帧的发送和接收。要注意的是数据帧是独立的网络信息传输单元
②网络层:负责将数据帧封装成IP数据包,并运行必要的路由算法
③传输层:负责端对端之间的通信会话连接和建立。传输协议的选择根据数据的传输方式而定
④应用层:负责应用程序的网络访问,这里通过端口号来识别各个不同的进程。
1.2 TCP/IP协议族
虽然TCP/IP名称包含了两个协议,但实际上,TCP/IP是一个庞大的协议族,它包含了各个层次上的众多协议,如图所示,举例了各层中的一些重要协议,并给出了各个协议在不同层次中所处的位置,如下所示。
ARP:用于获得同一物理网络中的硬件主机地址。
MPLS:多协议标签协议,是很有发展前景的下一代网络协议。
IP:负责在主机和网络之间寻址和路由数据包。
ICMP:用于发送有关数据包的传送错误的协议。
IGMP:被IP主机用来向本地多路广播路由器报告主机组成员的协议。
TCP:为应用程序提供可靠的通信连接。适合于一次传输大批数据的情况。并适用于要求得到响应的应用程序。
UDP:提供了无连接通信,且不对传送包进行可靠性保证。适合于一次传输少量数据,可靠性则由应用层来负责。
1.3 TCP和UDP
在此主要介绍在网络编程中涉及的传输层TCP和UDP。
1.TCP
(1)概述
同其他在任何协议栈一样,TCP向相邻的高层提供服务。因为TCP的上一层就是应用层,因此,TCP数据传输实现了从一个应用程序到另一个应用程序的数据传递。应用程序通过编程调用TCP并使用TCP服务,提供需要准备发送的数据,用来区分接收数据应用的目的地址和端口号。
通常应用程序通过打开一个socket来使用TCP服务,TCP管理到其他socket的数据传递。可以说,通过IP的源/目的可以唯一地区分网络中两台设备的连接,通过socket的源/目的可以唯一地区分网络中两个应用程序的连接。
(2)三次握手协议
TCP对话通过三次握手来进行初始化。三次握手的目的是使数据段的发送和接收同步,告诉其他主机其一次可接收的数据量,并建立虚连接。
TCP(Transmission Control Protocol) 传输控制协议。
TCP是主机对主机层的传输控制协议,提供可靠的连接服务,采用三次握手确认建立一个连接:
位码即tcp标志位,有6种标示:
SYN(synchronous建立联机) ACK(acknowledgement 确认) PSH(push传送) FIN(finish结束) RST(reset重置) URG(urgent紧急)
下面描述这三次握手的简单过程。
Sequence number(顺序号码) Acknowledge number(确认号码)
第一次握手:主机A发送位码为syn=1,随机产生seq number=1234567的数据包到服务器,主机B由SYN=1知道,A要求建立联机;
第二次握手:主机B收到请求后要确认联机信息,向A发送ack number=(主机A的seq+1),syn=1,ack=1,随机产生seq=7654321的包;
第三次握手:主机A收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ack是否为1,若正确,主机A会再发送ack number=(主机B的seq+1),ack=1,主机B收到后确认seq值与ack=1则连接建立成功。
完成三次握手,主机A与主机B开始传送数据
①初始化主机通过同步标志位置位的数据段发出会话请求。
②接收主机通过发回具有以下项目的数据段表示回复:同步标志置位、即将发送的数据段的起始字节的顺序号,应答并带有将收到的下一个数据段的字节顺序号。
③请求主机再回送一个数据段,并带有确认顺序号和确认号。
三次握手
(3)TCP包头数据
下图给出了TCP数据包头的格式。
SYN(synchronous建立联机) ACK(acknowledgement 确认) PSH(push传送) FIN(finish结束) RST(reset重置) URG(urgent紧急)
TCP数据包头的含义如下所示。
①源端口、目的端口:16位长。标识出远端和本地的端口号。
②序号:32位长。标识发送的数据报的顺序。
③确认号:32位长。希望收到的下一个数据包的序列号。
④TCP头长:4位长。表明TCP头中包含多少个32位字。
⑤6位未用。
⑥ACK:ACK位置1表明确认号是合法的。如果ACK为0,那么数据包不包含确认信息,确认字段被省略。
⑦PSH:表示是带有PUSH标志的数据。接收方因此请求数据包一到便将其送往应用程序而不必等到缓冲区装满时才传送。
⑧RST:用于复位由于主机崩溃或其他原因而出现的错误连接。还可以用于拒绝非法的数据包或拒绝连接请求。
⑨SYN:用于建立连接。
⑩FIN:用于释放连接。
窗口大小:16位长。窗口大小字段表示在确认了字节之后还可以发送多少个字节。
校验和:16位长。是为了确保高可靠性而设置的。它校验头部、数据和伪TCP头部之和。
可选项:0个或多个32位字。包括最大TCP载荷,滑动窗口比例以及选择重发数据包等选项。
2.UDP(User Datagram Protocol)
(1)概述
UDP即用户数据报协议,它是一种无连接协议,因此不需要像TCP那样通过三次握手来建立一个可靠的连接。同时,一个UDP应用可同时作为应用的客户或服务器方。由于UDP协议并不需要建立一个明确的连接,因此建立UDP应用要比建立TCP应用简单得多。
2. 网络基础编程
1. socket定义
在Linux中的网络编程是通过socket接口来进行的。socket是一种特殊的I/O接口,它也是一种文件描述符。socket是一种常用的进程之间通信机制,通过它不仅能实现本地机器上的进程之间的通信,而且通过网络能够在不同机器上的进程间进行通信。
每一个socket都用一个班相关描述{协议、本地地址、本地端口}来表示;一个完整的套接字则用一个相关描述{协议、本地地址、本地端口、远程地址、远程端口}来表示。
socket也有一个类似于打开文件的函数调用,该函数返回一个整型的socket描述符,随后的连接建立、数据传输等操作都是通过socket来实现的。
2. socket类型
常见的socket有3种类型如下。
(1)流式socket (SOCK_STREAM)
流式套接字提供可靠的、面向连接的通信流;它使用TCP协议,从而保证了数据传输的正确性和顺序性。
(2)数据报socket(SOCK_DGREAM)
数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且保证是可靠的、无差错的。它使用数据报协议UDP。
(3)原始socket
原始套接字允许对底层协议如IP或ICMP进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。(互联网控制消息协议(Internet Control Message Protocol,简称:ICMP)是TCP/IP协议族的一个子协议)
3. 地址及顺序处理
1.地址结构相关处理
(1)要使用到的结构体类型的介绍
下面先介绍两个重要的结构体类型:sockaddr和sockaddr_in,这两个结构类型都是用来保存socket信息的,如下所示:
struct sockaddr
{
unsigned short sa_family; /* 地址族 */
char sa_data[14]; /* 14字节的协议地址,包含该socket的IP地址和端口号 */
};
struct sockaddr_in
{
short int sa_family; /* 地址族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP地址 */
unsigned char sin_zero[8]; /* 填充0以保持与 struct sockaddr 同样大小 */
};
struct in_addr //用来保存以十六进制表示的IP地址
struct in_addr {unsigned long s_addr;
};
struct sockaddr和struct sockaddr_in是等效的,可以相互转化,通常sockaddr_in数据结构比较方便。
INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。一般来说,在各个系统中均定义成为0值。/* IP地址 */
bzero函数
原型:extern void bzero(void *s, int n);
参数说明:s 要置零的数据的起始地址; n 要置零的数据字节个数。
所需头文件 | #include <string.h> |
函数原型 | extern void bzero(void *s, int n); |
传入参数 | s 要置零的数据的起始地址 |
n 要置零的数据字节个数 |
(2)结构字段
表1列出了该结构sa_family字段可选的常见值。
sa_family可选值
结构定义头文件 | #include <netinet.h> |
sa_family | AF_INET: IPv4协议 |
AF_INET6: IPv6协议 | |
AF_LOCAL: UNIX域协议 | |
AF_LINK: 链路地址协议 | |
AF_KEY: 密钥套接字(socket) |
sockaddr_in 其他字段的含义非常清楚,具体的设置涉及其他函数,在后面会有详细的详解。
2.数据存储优先顺序
计算机数据存储有两种字节优先顺序:高字节优先(称为大端模式)和低字节优先(称为小端模式,PC机通常采用小端模式) 。Internet上数据以高位字节优先顺序在网络上传输,因此在有些情况下,需要对这两个字节存储优先顺序进行互相转化。这里用到4个函数:htons()、htonl()、
ntohs()、ntohl()。这4个地址分别实现网络字节序和主机字节序的转化,这里的h代表host,n代表network,s代表short,l表示long。通常16位的IP端口号用s表示,而IP地址用l来表示。
(2)函数格式说明
表2列出了这4个函数的语法格式。
htons等函数语法要点
htons函数:将一个16位的无符号短整型数据由主机分列体式格式转换成收集分列体式格式u_short htons(u_short hostshort),hostshort:默示一个主机分列体式格式的无符号整形数据。
所需头文件 | #include <netinet.h> |
函数原型 | uint16_t htons(uint16_t host16bits); uint32_t htonl(uint32_t host32bits); uint16_t ntohs(uint16_t net16bits); uint32_t ntohl(uint32_t net32bits); |
函数传入值 | host16bits:主机字节序的16位数据 |
host32bits:主机字节序的32位数据 | |
net16bits:网络字节序的16位数据 | |
net32bits:网络字节序的32位数据 | |
函数返回值 | 成功:返回要转换的字节序 |
失败:-1 |
注意:调用该函数只是使其得到相应的字节序,用户不需要清楚该系统的主机字节序和网络字节序是否真正相等。如果是相同不需要转换的话,该系统的这些函数会定义成空宏。
3.地址格式转化
(1)函数说明
通常用户在表示地址时采用的是点分十进制表示的数值(或者是以冒号分开的十进制IPv6地址),而在通常使用的socket编程中所使用的则是二进制值,这就需要将这两个数值进行转换。这里在IPv4中用到的函数有inet_aton()、inet_addr()和inet_ntoa(),而IPv4和IPv6兼容的函数都能inet_pton()和inet_ntop()。
这里inet_pton()函数是将点分十进制地址映射为二进制地址,而inet_ntop()是将二进制地址映射为点分十进制地址。
(2)函数格式
表3列出了inet_pton函数的语法要点。
inet_pton是一个IP地址转换函数,可以在将IP地址在“点分十进制”和“二进制整数”之间转换
char IPdotdec[20]; //存放点分十进制IP地址
struct in_addr s; // IPv4地址结构体
inet_pton(AF_INET, IPdotdec, (void *)&s);
inet_pton函数语法要点
所需头文件 | #include <arpa/inet.h> | |
函数原型 | int inet_pton(int family,const char *strptr,void *addptr); | |
函数传入值 | family | AF_INET:IPv4协议 |
AF_INET6:IPv6协议 | ||
strptr:要转化的值 | ||
addrptr:转化后的地址 | ||
函数返回值 | 成功:0 | |
失败:-1 |
表4列出了inet_ntop函数的语法要点。
inet_ntop()是将二进制地址映射为点分十进制地址。
inet_ntop函数语法要点
所需头文件 | #include <arpa/inet.h> | |
函数原型 | int inet_ntop(int family,void *addrptr,char *strptr,size_t len); | |
函数传入值 | family | AF_INET:IPv4协议 |
AF_INET6:IPv6协议 | ||
addrptr:转化后的地址 | ||
strptr:要转化的值 | ||
len:转化后的大小 | ||
函数返回值 | 成功:0 | |
失败:-1 |
4. 名字地址转化
(1)函数说明
通常,人们在使用过程中都不想记很长的IP地址,尤其到了IPv6时,地址长度多达128位,那时就更不可能一次次记住了。因此,使用主机名将会是最好的方法。
在Linux中,同样有一些函数可以实现主机名和地址的转化,最常见的有gethostbyname()、
gethostbyaddr()和getaddrinfo()等,它们都可以实现IPv4和IPv6的地址和主机名之间的转化。
①gethostbyname()是将主机名转化为IP地址
②gethostbyaddr()则是逆操作,将IP地址转化为主机名
③getaddrinfo()还能实现自动识别IPv4地址和IPv6地址
gethostbname()和gethostbyaddr()都涉及一个hostent的结构体,如下所示:
hostent是host entry的缩写,该结构记录主机的信息,包括主机名、别名、地址类型、地址长度和地址列表。之所以主机的地址是一个列表的形式,原因是当一个主机有多个网络接口时,自然有多个地址。
hostent的定义如下:
- struct hostent
- {
- char *h_name; //正式主机名
- char **h_aliases; //主机别名
- int h_addrtype; //主机IP地址类型:IPV4-AF_INET
- int h_length; //主机IP地址字节长度,对于IPv4是四字节,即32位
- char **h_addr_list; //主机的IP地址列表
- };
- #define h_addr h_addr_list[0] //保存的是IP地址
参数分析:
①h_name:正式主机名
②h_aliases:主机别名
③h_addrtype:地址类型
④h_length:地址字节长度
⑤h_addr_list:指向IPv4或IPv6的地址指针数组
调用gethostbyname()函数或gethostbyaddr()函数后就能返回hostent结构体的相关信息。
getaddrinfo()函数涉及一个addrinfo的结构体,如下所示:
typedef struct addrinfo {
int ai_flags; //AI_PASSIVE,AI_CANONNAME,AI_NUMERICHOST
int ai_family; //AF_INET,AF_INET6
int ai_socktype; //SOCK_STREAM,SOCK_DGRAM
int ai_protocol; //IPPROTO_IP, IPPROTO_IPV4, IPPROTO_IPV6 etc.
size_t ai_addrlen; //地址字节长度
char* ai_canonname; //主机名
struct sockaddr* ai_addr; //socket结构体
struct addrinfo* ai_next; //下一个指针链表
} ;
就hostent结构体而言,addrinfo结构体包含更多的信息。
(2)函数格式
表5列出了gethostbyname()函数的语法要点
gethostbyname()函数主要作用:用域名或者主机名获取地址
表5 gethostbyname函数语法要点
所需头文件 | #include <netdb.h> |
函数原型 | struct hostent *gethostbyname(const char *hostname); |
函数传入值 | hostname:主机名 |
函数返回值 | 成功:hostent类型指针 |
出错:-1 |
调用该函数时可以首先对h_addrtype和h_length进行设置,若为IPv4可设置为AF_INET和4;若为IPv6可设置为AF_INET6和16;若不设置默认为IPv4地址类型。
(2)getaddrinfo()
表6列出了getaddrinfo函数的语法要点
getaddrinfo函数能够处理名字到地址以及服务到端口这两种转换,返回的是一个sockaddr结构的链表而不是一个地址清单。
getaddrinfo()函数语法要点
所需头文件 | #include <netdb.h> |
函数原型 | int getaddrinfo(const char *node,const char *service,const struct addrinfo *hints,struct addrinfo **result); |
函数传入值 | node:网络地址或网络主机名 |
service:服务名或十进制的端口号字符串 | |
hints:服务线索 | |
result:返回结果 | |
函数返回值 | 成功:0 |
失败:-1 |
在调用之前,首先要对hints服务线索进行设置。它是一个addrinfo结构体,表7列举了该结构体常见的选项值。
表7 addrinfo结构体常见选项值
结构体头文件 | #include <netdb.h> |
ai_flags | AI_PASSIVE:该套接口是用作被动地打开 |
AI_CANONNAME:通知getaddrinfo函数返回主机的名字 | |
ai_family | AF_INET:IPv4协议 |
AF_INET6:IPv6协议 | |
AF_UNSPEC:IPv4或IPv6均可 | |
ai_socktype | SOCK_STREAM:字节流套接字socket(TCP) |
SOCK_DGEAM:数据报套接字socket(UDP) | |
ai_protocol | IPPROTO_IP:IP协议 |
IPPROTO_IPv4:IPv4协议 | |
IPPROTO_IPv6:IPv6协议 | |
IPPROTO_UDP:UDP | |
IPPROTO_TCP:TCP |
注意:
(1)通常服务器端在调用getaddrinfo()之前,ai_flag设置AI_PASSIVE,用于bind()函数(用于端口和地址的绑定,后面会讲到),主机名nodename通常会设置为NULL。
(2)客户端调用getaddrinfo()时,ai_flags一般不设置AI_PASSIVE,但是主机名nodename和服务器servname(端口)则应该不为空。
(3)即使不使用ai_flags为AI_PASSIVE,取出的地址也可以被绑定,很多程序中ai_flags直接为0,即3个标志位都不设置,这种情况下只要hostname和servname设置的没有问题就可以正确绑定。
(3)使用实例。
下面实例给出了getaddrinfo函数用法的示例,在后面小节中会给出gethostbyname函数用法的例子。
/* getaddrinfo.c */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main()
{
struct addrinfo hints,*res = NULL;
int rc;
memset(&hints,0,sizeof(hints));
/* 设置addrinfo结构体中各参数 */
hints.ai_flags = AI_CANONNAME;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
/* 调用getaddrinfo函数 */
rc = getaddrinfo("localhost",NULL,&hints,&res);
if(rc != 0)
{
perror("getaddrinfo");
exit(1);
}
else
{
printf("Host name is %s\n",res->ai_canonname);
}
exit(0);
}
3 socket基础编程
(1)函数说明。
socket网络编程的基本函数socket()、bind()、listen()、accept()、send()、sendto()、recv()以及recvfrom()等,其中根据客户端还是服务端,或者根据TCP协议还是UDP协议,这些函数的调用流程都有所区别,这里先对每个函数进行说明,再给各种情况下使用的流程图。
①socket():该函数用于建立一个socket连接,可指定socket类型消息。在建立了socket连接之后,可对sockaddr或sockaddr_in结构进行初始化,以保存所建立的socket地址信息。
②bind():该函数是用于将本地IP地址绑定到端口号,若绑定其他IP地址则不能成功。另外,它主要用于TCP的连接,而在UDP的连接中则无必要。
③listen():在服务端程序成功建立套接字和与地址进行绑定之后,还需要准备在该套接字上接收新的连接请求。此时调用listen()函数来创建一个等待队列,在其中存放未处理的客户端连接请求。
④accept():服务端程序调用listen()函数创建等待队列之后,调用accpet()函数等待并接收客户端的连接请求。它通常从由bind(
)所创建的等待队列中取出第一个未处理的连接请求。
⑤connect():该函数在TCP中是用于bind()的之后的client端,用于与服务器端建立连接,而在UDP中由于没有了bind()函数,因此用connect()有点类似bind()函数的作用。
⑥send()和recv():这两个函数分别用于发送和接收数据,可以用在TCP中,也可以用在UDP中。当用在UDP时,可以在connect()函数建立连接之后再用。
⑦sendto()和recvfrom():这两个函数的作用与send()和recv()类似,也可以用在TCP和UDP中。当用在TCP时,后面的几个与地址有关参数不起作用,函数作用等同于send()和recv();当用在UDP时,可以用在之前没使用connect()的情况下,这两个函数可以自动寻找指定的地址并进行连接。
服务器端和客户端使用TCP协议的流程如图所示
服务器端和客户端使用UDP协议的流程
表8列出了socket()函数的语法要点。
socket()函数用于根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源。
表8 socket()函数语法要点
所需要的头文件 | #include <sys/socket.h> | |
函数原型 | int socket(int family,int type,int potocol); | |
函数传入值 | family: 协议族 | AF_INET : IPv4协议 |
AF_INET6 : IPv6协议 | ||
AF_LOCAL : UNIX域协议 | ||
AF_ROUTE:路由套接字(socket) | ||
AF_KEY:密钥套接字(socket) | ||
type: 套接字类型 | SOCK_STREAM:字节流套接字 socket | |
SOCK_DGRAM:数据报套接字 socket | ||
SOCK_RAW:原始套接字 socket | ||
protocol:0(原始套接字除外) | ||
函数返回值 | 成功:非负套接字描述符 | |
出错:-1 |
表9 列出了bind()函数的语法要点 bind—— 紧拴
bind()是计算机编程中的一个函数,用于未连接的数据报或流类套接口,把一本地地址与一套接口捆绑,在connect()或listen()调用前使用。
表9 bind()函数语法要点
所需头文件 | #include <sys/socket.h> |
函数原型 | int bind(int sockfd,struct sockaddr *my_addr,int addrlen); |
函数传入值 | sockfd:套接字描述符 |
my_addr:本地地址 | |
addrlen:地址长度 | |
函数返回值 | 成功:0 |
出错:-1 |
端口号和地址在my_addr中给出了,若不指定地址,则内核随意分配一个临时端口给该应用程序。
表10 列出了listen()函数的语法要点。
listen在套接字函数中表示让一个套接字处于监听到来的连接请求的状态
表10 listen()函数语法要点
所需头文件 | #include <sys/socket.h> |
函数原型 | int listen(int sockfd,int backlog); |
函数传入值 | sockfd:套接字描述符 |
backlog:请求队列中允许的最大请求数,大多数系统默认值为5 | |
函数返回值 | 成功:0 |
出错:-1 |
表11 列出了accept()函数的语法要点。
函数从sockfd的等待连接队列中抽取第一个连接,创建一个与sockfd同类的新的套接口并返回句柄。
表11 accept()函数语法要点
所需头文件 | #include <sys/socket.h> |
函数原型 | int accept(int sockfd,struct sockaddr *client_addr,socklen_t *addrlen); |
函数传入值 | socktd:套接字描述符 |
client_addr:客户端地址 | |
addrlen:地址长度 | |
函数返回值 | 成功:返回客户端描述符 |
失败:-1 |
表12 列出了connect()函数的语法要点
表12 connect()函数语法要点
所需头文件 | #include <sys/socket.h> |
函数原型 | int connect(int sockfd,struct sockaddr *server_addr,int addrlen); |
函数传入值 | sockfd:套接字描述符 |
server_addr:服务器地址 | |
addrlen:地址长度 | |
函数返回值 | 成功:0 |
失败:-1 |
表13 send()函数的语法特点
表13 send()函数语法要点
所需头文件 | #include <sys/socket.h> |
函数原型 | int send(int sockfd,const void *msg,int len,int flags); |
函数传入值 | sockfd:套接字描述符 |
msg:指向要发送数据的指针 | |
len:数据长度 | |
flags:一般为0 | |
函数返回值 | 成功:发送的字节数 |
失败:-1 |
表14 列出recv()函数的语法要点
表14 recv()函数语法要点
所需头文件 | #include <sys/socket.h> |
函数原型 | int recv(int sockfd,void *buf,int len,unsigned int flags); |
函数传入值 | sockfd:套接字描述符 |
msg:指向要接收数据的缓冲区 | |
len:数据长度 | |
flags:一般为0 | |
函数返回值 | 成功:接收的字节数 |
出错:-1 |
表15 列出了sendto()函数的语法要点。
表15 sendto()函数语法要点
所需的头文件 | #include <sys/socket.h> |
函数原型 | int sendto(int sockfd,const void *msg,int len,unsigned int flags,const struct sockaddr *to,int tolen); |
函数传入值 | sockfd:套接字描述符 |
msg:指向要发送数据的指针 | |
len:数据长度 | |
flags:一般为0 | |
to:目地机的IP地址和端口号信息 | |
tolen:地址长度 | |
返回值 | 成功:发送的字节数 |
失败:-1 |
目地机:接收端
IP地址(Internet Protocol Address),全称为网际协议地址,是一种在Internet上的给主机编址的方式。它是IP协议提供的一种统一的地址格式,常见的IP地址分为IPv4与IPv6两大类,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。
所谓的端口,就好像是门牌号一样,客户端可以通过ip地址找到对应的服务器端,但是服务器端是有很多端口的,每个应用程序对应一个端口号,通过类似门牌号的端口号,客户端才能真正的访问到该服务器。为了对端口进行区分,将每个端口进行了编号,这就是端口号。
表16 列出了recvfrom()函数的语法要点。
表16 recvfrom()函数语法要点
所需头文件 | #include <sys/socket.h> |
函数原型 | int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen) |
函数传入值 | sockfd:套接字描述符 |
buf:存放接收数据的缓冲区 | |
len:数据长度 | |
flags:一般为0 | |
from:源主机的IP地址和端口号信息 | |
tolen:地址长度 | |
函数返回值 | 成功:接收的字节数 |
出错:-1 |
源主机:发送端
(3)使用实例。
该实例分为客户端和服务器端两部分,其中服务器端首先建立起socket,然后与本地端口进行绑定,接着就开始从客户端的连接请求并建立与它的连接,接下来,接收客户端发送的信息。客户端则在建立socket之后调用connect()函数来建立连接。
所需头文件 | #include <sys/socket.h> |
函数原型 | int setsockopt(int s,int level,int optname, const char *optval,int optlen); |
传入参数 | s:标识一个套接字的描述符。 |
level:选项定义的层次;目前仅支持SOL_SOCKET和IPPROTO_TCP层次。 | |
optname:需设置的选项。 | |
optval:指针,指向存放选项值的缓冲区。 | |
optlen:optval缓冲区长度。 | |
作用 | 设置套接字的选项 |
bzero函数
原型:extern void bzero(void *s, int n);
参数说明:s 要置零的数据的起始地址; n 要置零的数据字节个数。
所需头文件 | #include <string.h> |
函数原型 | extern void bzero(void *s, int n); |
传入参数 | s 要置零的数据的起始地址 |
n 要置零的数据字节个数 |
服务端的代码如下所示:
void *memset(void *s, int ch,size_t n);
函数解释:将s中当前位置后面的n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s .
memset:作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
#define h_addr h_addr_list[0] //保存的是IP地址
/* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#define PORT 4321
#define BUFFER_SIZE 1024
#define MAX_QUE_CONN_NM 5
int main()
{
struct sockaddr_in server_sockaddr,client_sockaddr;
int sin_size,recvbytes;
int sockfd,client_fd;
char buf[BUFFER_SIZE];
/* 建立socket连接 */
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror("socket");
exit(1);
}
printf("Socket id=%d\n",sockfd);
/* 设置sockaddr_in结构体中相关参数 */
server_sockaddr.sin_family=AF_INET;
server_sockaddr.sin_port=htons(PORT);
server_sockaddr.sin_addr.s_addr=INADDR_ANY;
bzero(&(server_sockaddr.sin_zero),8);
int i=1;/* 允许重复使用本地地址与套接字进行绑定 */
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&i,sizeof(i));
/* 绑定函数bind() */
if(bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr))==-1)
{
perror("bind");
exit(1);
}
printf("Bind success!\n");
/* 调用listen()函数,创建未处理请求的队列 */
if(listen(sockfd,MAX_QUE_CONN_NM)==-1)
{
perror("listen");
exit(1);
}
printf("Listen success!\n");
/* 调用accept()函数,等待客户端的连接 */
if((client_fd=accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size))==-1)
{
perror("accept");
exit(1);
}
/* 调用recv()函数接收客户端的请求 */
memset(buf,0,sizeof(buf));
if((recvbytes = recv(client_fd,buf,BUFFER_SIZE,0))==-1)
{
perror("recv");
exit(1);
}
printf("Received a message: %s\n",buf);
close(sockfd);
exit(1);
}
客户端代码如下:
/* client.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <unistd.h>
#define PORT 4321
#define BUFFER_SIZE 1024
int main(int argc,char *argv[])
{
int sockfd,sendbytes;
struct sockaddr_in serv_addr;
char buf[BUFFER_SIZE];
struct hostent *host;
if(argc<3)
{
fprintf(stderr,"USAGE:./client Hostname (or ip address) Text\n");
exit(1);
}
/* 地址解析函数 */
if((host=gethostbyname(argv[1]))==NULL)
{
perror("gethostbyname");
exit(1);
}
memset(buf,0,sizeof(buf));
sprintf(buf,"%s",argv[2]);
/*创建socket*/
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror("socket");
exit(1);
}
/*设置sockaddr_in结构体中相关参数*/
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(PORT);
serv_addr.sin_addr=*((struct in_addr *)host->h_addr);
bzero(&(serv_addr.sin_zero),8);
/* 调用connect函数主动发起对服务器端的连接 */
if(connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr)==-1))
{
perror("connect");
exit(1);
}
/* 发送消息给服务器 */
if(sendbytes=send(sockfd,buf,BUFFER_SIZE,0)==-1)
{
perror("send");
exit(1);
}
close(sockfd);
exit(1);
}
3. 网络高级编程
在实际情况中,人们往往遇到多个客户端连接服务器端的情况。由于之前介绍如connect()、recv()和send()等都是阻塞性函数,如果资源没有准备好,则调用函数的进程将进入睡眠状态,这样就无法处理I/O多路复用的情况了。这里给出了解决I/O多路复用的解决方法,这两个函数都是之前学过的fcntl()和select(),由于在Linux中把socket也作为一种文件描述符,这给用户的处理带来了很大的方便。
1.fcntl()函数
函数fcntl()针对socket编程提供了如下的编程特性。
①非阻塞I/O:可将cmd设置为F_SETFL,将lock设置为O_NONBLOCK。
②异步I/O:可将cmd设置为F_SETFL,将lock设置为O_ASYNC。
下面是用fcntl()将套接字设置为非阻塞I/O的实例代码:
/* net_fcntl.c */
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/un.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <fcntl.h>
#define PORT 1234
#define MAX_QUE_CONN_NM 5
#define BUFFER_SIZE 1024
int main()
{
struct sockaddr_in server_sockaddr,client_addr;
int sockfd,client_fd;
int sin_size,recvbytes,flags;
char buf[BUFFER_SIZE];
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror("socket");
exit(1);
}
server_sockaddr.sin_family=AF_INET;
server_sockaddr.sin_port=htons(PORT);
server_sockaddr.sin_addr.s_addr=INADDR_ANY;
bzero(&(server_sockaddr.sin_zero),8);
int i=1;/* 允许重复使用本地地址与套接字进行绑定 */
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&i,sizeof(i));
if(bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr))==-1)
{
perror("bind");
exit(1);
}
if(listen(sockfd,MAX_QUE_CONN_NM)==-1)
{
perror("listen");
exit(1);
}
printf("Listening....\n");
/* 调用fcntl()函数给套接字设置非阻塞属性 */
flags=fcntl(sockfd,F_GETFL);
if(flags<0 || fcntl(sockfd,F_SETFL,flags|O_NONBLOCK)<0)
{
perror("fcntl");
exit(1);
}
while(1)
{
sin_size = sizeof(struct sockaddr_in);
if((client_fd=accept(sockfd,(struct sockaddr *)&client_addr,&sin_size))<0)
{
perror("accept");
exit(1);
}
if((recvbytes=recv(client_fd,buf,BUFFER_SIZE,0))<0)
{
perror("recv");
exit(1);
}
printf("Received a message: %s\n",buf);
}
close(client_fd);
exit(1);
}
运行该程序,结果如下所示:
可以看到,当accept()的资源可不用(没有任何未处理的等待连接的请求)时, 程序就会自动返回。
2.select()
使用fcntl()函数虽然可以实现非阻塞I/O或信号驱动I/O,但在实际使用时往往会对资源是否准备完毕进行循环测试,这样就大大增加了不必要的CPU资源的占用。在这里可以使用select()函数来解决这个问题,同时,使用select()函数还可以设置等待的时间,可以说功能更加强大。下面是使用select()函数的服务器端源代码。客户端程序基本上与上面的程序相同,仅加入一行sleep()函数,使得客户端进程等待几秒才结束。
select函数解析
select 函数声明如下
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int maxfd, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数说明、
- maxfds:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错。在linux系统中,select的默认最大值为1024。设置这个值的目的是为了不用每次都去轮询这1024个fd,假设我们只需要几个套接字,我们就可以用最大的那个套接字的值加上1作为这个参数的值,当我们在等待是否有套接字准备就绪时,只需要监测maxfd+1个套接字就可以了,这样可以减少轮询时间以及系统的开销。
- readfds:首先需要明白,fd_set是什么数据类型,有一点像int,又有点像struct,其实,fd_set声明的是一个集合,也就是说,readfs是一个容器,里面可以容纳多个文件描述符,把需要监视的描述符放入这个集合中,当有文件描述符可读时,select就会返回一个大于0的值,表示有文件可读;、
- writefds:和readfs类似,表示有一个可写的文件描述符集合,当有文件可写时,select就会返回一个大于0的值,表示有文件可写;
- fd_set*errorfds同上面两个参数的意图,用来监视文件错误异常文件。
- timeout:这个参数一出来就可以知道,可以选择阻塞,可以选择非阻塞,还可以选择定时返回。当将timeout置为NULL时,表明此时select是阻塞的;当将tineout设置为timeout->tv_sec = 0,timeout->tv_usec = 0时,表明这个函数为非阻塞;当将timeout设置为非0的时间,表明select有超时时间,当这个时间走完,select函数就会返回。从这个角度看,个人觉得可以用select来做超时处理,因为你如果使用recv函数的话,你还需要去设置recv的模式,麻烦的很。
struct timeval{
long tv_sec; /*秒 */
long tv_usec; /*微秒 */
}
下面我们介绍下操作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);
对fd_set的理解:fd_set可以理解为一个集合,那么集合就会有一个数量,在<sys/select.h>总定义了一个常量FD_SETSIZE,默认为1024,也就是说在这个集合内默认最多有1024个文件描述符,但是通常你用不了这么多,你通常只是关心maxfds个描述符。也就是说你现在有maxfds个文件描述符在这个集合里,那么我怎么知道集合里的哪个文件描述符有消息来了呢?你可以将fd_set中的集合看成是二进制bit位,一位代表着一个文件描述符。0代表文件描述符处于睡眠状态,没有数据到来;1代表文件描述符处于准备状态,可以被应用层处理。我觉得select函数可以分下面几步进行理解
- 在你开始监测这些描述符时,你先将这些文件描述符全部置为0
- 当你需要监测的描述符置为1
- 使用select函数监听置为1的文件描述符是否有数据到来
- 当状态为1的文件描述符有数据到来时,此时你的状态仍然为1,但是其他状态为1的文件描述如果没有数据到来,那么此时会将这些文件描述符置为0
- 当select函数返回后,可能有一个或者多个文件描述符为1,那么你怎么知道是哪个文件描述符准备好了呢?其实select并不会告诉你说,我哪个文件描述符准备好了,他只会告诉你他的那些bit为位哪些是0,哪些是1。也就是说你需要自己用逻辑去判断你要的那个文件描是否准备好了
理解了上面几步的话,下面这些宏就比较好理解了。
- FD_ZERO:将指定集合里面所有的描述符全部置为0,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于在系统分配内存空间后,通常并不作清空处理,所以结果是不可知的
- FD_SET:用于在文件描述符集合中增加一个新的文件描述符,将相应的位置置为1
- FD_CLR:用来清除集合里面的某个文件描述符
- FD_ISSET:用来检测指定的某个描述符是否有数据到来。- 那么假如在我们的程序中有5个客户端已经连接上了服务器,这个时候突然有一条数据过来了。select返回了,但是此时你并不知道是哪个客户发过来的消息,因为你每个客户发过来的消息都是一样重要的。所以你没法去只针对一个套接字使用FD_ISSET,你需要做的是用一个循环去检测(FD_ISSET)到底是哪一个客户发过来的消息,因为如果此时你监测一个套接字的话,其他客户的信息你会丢失。这个也是select的一个缺点,你需要去检测所有的套接字,看看这个套接字到底是谁来的数据。
select函数解析和理解大概就讲到这里了。下面讲一下select的使用以及示例
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/un.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <fcntl.h>
#define PORT 4321
#define MAX_QUE_CONN_NM 5
#define MAX_SOCK_FD FD_SETSIZE
#define BUFFER_SIZE 1024
int main()
{
int sockfd,client_fd,fd;
struct sockaddr_in server_sockaddr,client_sockaddr;
fd_set inset,tmp_inset;
int sin_size,count;
char buf[BUFFER_SIZE];
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror("socket");
exit(1);
}
server_sockaddr.sin_family=AF_INET;
server_sockaddr.sin_port=htons(PORT);
server_sockaddr.sin_addr.s_addr=INADDR_ANY;
bzero(&(server_sockaddr.sin_zero),8);
int i=1;/*重复使用本地地址于套接字进行绑定*/
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&i,sizeof(i));
if((bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr))) == -1)
{
perror("bind");
exit(1);
}
if((listen(sockfd,MAX_QUE_CONN_NM))==-1)
{
perror("listen");
exit(1);
}
printf("lsitening....\n");
FD_ZERO(&inset);
FD_SET(sockfd,&inset);
while(1)
{
tmp_inset=inset;
sin_size=sizeof(struct sockaddr_in);
memset(buf,0,sizeof(buf));
if(!(select(MAX_SOCK_FD,&tmp_inset,NULL,NULL,NULL)>0))
{
perror("select");
}
for(fd=0;fd<MAX_SOCK_FD;fd++)
{
if(fd==sockfd)
{
if((client_fd=accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size))==-1)
{
perror("accept");
exit(1);
}
FD_SET(client_fd,&inset);
printf("New connection from %d(socket)\n",client_fd);
}
else
{
if((count = recv(client_fd,buf,BUFFER_SIZE,0))>0)
{
printf("Received a message from %d:%s\n",client_fd,buf);
}
else
{
close(fd);
FD_CLR(fd,&inset);
printf("CLient %d(socket) has left\n",fd);
}
}
}
}
close(sockfd);
exit(1);
}