第十章 嵌入式Linux网络编程
10.1 TCP/IP协议概述
10.1.1 OSI参考模型及TCP/IP参考模型
7层转4层
10.1.2 TCP/IP协议族
应用层[telnet | ftp ]传输层[TCP|UDP]网络层[ICMP|IGMP|IPv4 IPv6]网络接口层[ARP RARP |MPLS]
ARP:用于获得同一物理网络中的硬件主机地址
MPLS:多协议标签协议,是很有发展前景的下一代网络协议
IP:负责在主机和网络之间寻址和路由数据包
ICMP:用于发送报告有着数据包的传送错误的协议
IGMP:被IP主机用来向本地多路广播路由器报告主机组成员的协议
TCP:为应用程序提供可靠的通信连接。适合于一次传输在批数据的情况。并适用于要求得到响应的应用程序。
UDP:提供了无连接通信,且不对传送包进行可靠的保证。适合于一次传输少量数据,可靠性则由应用层来负责
10.1.3 TCP和UDP
10.2 网络基础编程
10.2.1 socket概述
在Linux中的网络编程是通过socket接口来进行的。是一种文件描述符。socket也有一个类似于打开文件的函数调用,该函数返回一个整型的socket描述符,随后的连接建立、数据传输等操作都是通过socket来实现的。
2. socket类型
常见的socket有3种类型:
(1)流式socket (SOCK_STREAM) 流式套接字提供可靠的、面向连接的通信流;它使用TCP协议,从而保证了数据传输的正确性和顺序性。
(2)数据报socket(SOCK_DGRAM) 数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议UDP
(3)原始socket 原始套接字允许对底层协议如IP或ICMP进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。
10.2.2 地址及顺序处理
1. 地址结构相关处理
sockaddr和sockaddr_in,这两个结构类型都是用来保存socket信息的,
struct sockaddr{
unsigned short sa_family;
char sa_data[14];
};
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同样大小*/
};
sa_family: AF_INET(IPv4) | AF_INET6(IPv6) | AF_LOCAL(UNIX域协议) | AF_LINK(链路地址协议) | AF_KEY(密钥套接字)
2. 数据存储优先顺序
(1)函数说明:计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。Internet上数据以高位字节优先顺序在网络上传输,因此在有些情况下,需要对这两个字节存储优先顺序进行相互转化。这里用到了四个函数:htons、ntohs、htonl、ntohl。这四个地址分别实现网络字节序和主机字节充的转化,这里的h代表host,n代表network,s代表short,l代表long。通常16位的IP端口号用s代表,而IP地址用l来代表。
(2)函数格式说明
uint16_t htons(unit16_t host16bit) 参数是主机字节序的16bit数据
uint32_t htonl(unit32_t host32bit) 参数是主机字节序的32bit数据
uint16_t ntohs(unit16_t net16bit) 参数是网络字节序的16bit数据
uint32_t ntohs(unit32_t net32bit) 参数是网络字节序的32bit数据
3. 地址格式转化
通常用户在表达地址时采用的是点分十进制表示的数值(或者是以冒号分开的十进制IPv6地址),而在通常使用的socket编程中所使用的则是十进制值,这就需要将这两个数值进行转换。
IPv4中用到的函数有inet_aton、inet_addr和inet_ntoa
IPv4和IPv6兼容的函数有inet_pton和inet_ntop,这里,p表示十进制,n表示二进制。
int inet_pton(int family, const char *strptr, void *addrptr)
int inet_ntop(int family, void *addrptr, char *strptr, size_t len)
family传入AF_INET或AF_INET6,addrptr是转化后的地址,strptr是要转化的值,len是转化后值的大小,成功返回0,出错返回-1
4.名字地址转化
(1)函数说明
IP地址不愿意记忆,用函数实现主机名和地址的转化,如:gethostbyname、gethostbyaddr、getaddrinfo等,它们都可以实现IPv4和IPv6的地址和主机名之间的转化。
gethostbyname和gethostbyaddr都涉及到一个hostent的结构体
struct hostent{
char *h_name;/*正式主机名*/
char **h_aliases;/*主机别名*/
int h_addrtype;/*地址类型,AF_INET | AF_INET6 */
int h_length;/*地址长度,4 | 16 */
char **h_addr_list;/*指向IPv4或IPv6的地址指针数组*/
}
getaddrinfo函数涉及到一个addrinfo的结构体
struct addrinfo{
int ai_flags;/*AI_PASSIVE, AI_CANONNAME*/
int ai_family;/*AF_INET, AF_INET6, AF_UNSPE(IPv4 IPv6均可)*/
int ai_socktype;/*SOCK_STREAM, SOCK_DGRAM*/
int ai_protocol;/*协议类型 IPPROTO_IP, IPPROTO_IPV4, IPPROTO_IPV6, IPPROTO_UDP, IPPROTO_TCP*/
size_t ai_addrlen;/**/
char *ai_canoname;/*主机名*/
struct sockaddr *ai_addr;/*socket结构体*/
struct addrinfo *ai_next;/*下一个指针链表*/
}
(2)函数格式
struct hostent *gethostbyname(const char *hostname)
int getaddrinfo(const char *hostname/*主机名*/, const char * service/*服务名或十进制的串口号字符串*/, const struct addrinfo *hints/*服务线索*/, struct addrinfo **result/*返回结果*/)
调用getaddrinfo之前,首先要对hints服务线索进行设置,它是一个addrinfo结构体。
(3)使用实例
10.2.3 socket基础编程
(1)函数说明
进行socket编程的基本函数有socket、bind、listen、accept、send、sendto、recv、recvfrom这几个
其中对于客户端和服务器端以及TCP和UDP的操作流程都有所区别。
socket:该函数用于建立一个 socket 连接,可指定 socket 类型等信息。在建立了 socket连接之后,可对 socketadd 或 sockaddr_in 进行初始化,以保存所建立的 socket 信息。
bind:该函数是用于将本地 IP 地址绑定端口号的,若绑定其他地址则不能成功。另外,它主要用于 TCP 的连接,而在 UDP 的连接中则无必要。
connect:该函数在TCP中是用于 bind 的之后的 client 端,用于与服务器端建立连接,而在 UDP 中由于没有了 bind 函数,因此用 connect 有点类似 bind 函数的作用。
send和recv: 这两个函数用于接收和发送数据,可以用在 TCP 中,也可以用在 UDP中。当用在 UDP 时,可以在 connect 函数建立连接之后再用。
sendt和recvfrom: 这两个函数的作用与 send 和 recv 函数类型,也可以用在 TCP 和UDP 中。当用在 TCP 时,后面的几个与地址有关参数不起作用,函数作用等同于 send 和 recv;当用在 UDP 时,可以用在之前没有使用 connect 的情况时,这两个函数可以自动寻找制定地址并进行连接。
图 10.6 使用 TCP 协议 socket 编程流程图

图 10.7 使用 UDP 协议 socket 编程流程图

int socket(int family, int type, int protocol)
int bind(int socketfd, struct sockaddr *my_addr(本地地址), int addrlen(地址长度)) 若不指定地址,则内核随意分配一个临时端口给该应用程序
int listen(int sockfd, int backlog(请求队列中允许的最大请求数,大多数系统缺省值为20))
int accept(int sockfd, struct sockaddr *addr(客户端地址), socklen_t *addrlen(地址长度))
int connect(int sockfd, struct sockaddr *serv_addr(服务器端地址), int addrlen)
int send(int sockfd, const void *msg, int len, int flags)
int recv(int sockfd, void *buf, int len, unsigned int flags)
int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen(地址长度))
int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen(地址长度))
(3)使用实例
Service
Client
#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> #define SERVPORT 3333 #define MAXDATASIZE 100 main(int argc,char *argv[]){ int sockfd,sendbytes; char buf[MAXDATASIZE]; struct hostent *host; struct sockaddr_in serv_addr; if(argc < 2){ fprintf(stderr,"Please enter the server's hostname!\n"); exit(1); } fprintf(stdout,"ip = %s\n",argv[1]); /*地址解析函数*/ if((host=gethostbyname(argv[1]))==NULL){ perror("gethostbyname"); exit(1); } /*创建 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(SERVPORT); 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,"hello",6,0))== -1){ perror("send"); exit(1); } close(sockfd); }
10.3 网络高级编程
在实际情况中,人们往往遇到多个客户端连接服务端的情况。由于之前介绍的如connet、recv、send都是阻塞性函数,若资源没有准备好,则调用该函数的进程将进入睡眠状态,这样就无法处理I/O多路复用的情况了。
本节给出两种解决I/O多路得用的解决方法,fcntl和select。
可以看到,由于在Linux中把socket也作为一种特殊文件描述符,这给用户的处理带来了很大方便。
1.fcntl
非阻塞I/O:可将cmd设置为F_SETFL,将lock设置为O_NONBLOCK
信息驱动I/O:可将cmd设置为F_SETFL,将lock设置为O_ASYNC
2.select
使用fcntl函数虽然可以实现非阻塞I/O或信号驱动I/O,但在实际使用时往往会对资源是否准备完毕进行循环测试,这样就大大增加了不必要的CPU资源。
在这里可以使用select函数来解决这个问题,同时,使用select函数还可以设置等待时间,可以说功能更加强大。
select_socket
#include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<unistd.h> #include<stdio.h> #include<stdlib.h> #include<string.h> #define PORT 1234 #define MAXSOCKFD 10 int main(int argc, char **argv) { int sockfd,newsockfd,is_connected[MAXSOCKFD],fd; struct sockaddr_in addr; int addr_len = sizeof(struct sockaddr_in); fd_set readfds; char buffer[256]; char msg[ ] ="Welcome to server!"; if ((sockfd = socket(AF_INET,SOCK_STREAM,0))<0) { perror("socket"); exit(1); } bzero(&addr,sizeof(addr)); addr.sin_family =AF_INET; addr.sin_port = htons(PORT); addr.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(sockfd,&addr,sizeof(addr))<0) { perror("connect"); exit(1); } if(listen(sockfd,3)<0) { perror("listen"); exit(1); } for(fd=0;fd<MAXSOCKFD;fd++) is_connected[fd]=0; while(1) { FD_ZERO(&readfds); FD_SET(sockfd,&readfds); //用于侦听新连接 for(fd=0;fd<MAXSOCKFD;fd++) { if(is_connected[fd]) FD_SET(fd,&readfds); //已处理的连接加入select侦听 } if(!select(MAXSOCKFD,&readfds,NULL,NULL,NULL)) continue; for(fd=0;fd<MAXSOCKFD;fd++) { if(FD_ISSET(fd,&readfds)) { if(sockfd ==fd) //一个未处理的连接 { if((newsockfd = accept(sockfd,&addr,&addr_len))<0) perror("accept"); write(newsockfd,msg,sizeof(msg)); is_connected[newsockfd] =1; printf("cnnect from %s\n",inet_ntoa(addr.sin_addr)); } else //已处理的连接有读就绪 { bzero(buffer,sizeof(buffer)); if(read(fd,buffer,sizeof(buffer))<=0) //读数据失败,表明连接关闭 { printf("connect closed.\n"); is_connected[fd]=0; close(fd); } else //成功读取数据 printf("%s",buffer); } } } } }
client
#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> #define SERVPORT 1234// 3333 #define MAXDATASIZE 100 main(int argc,char *argv[]){ int sockfd,sendbytes; char buf[MAXDATASIZE]; struct hostent *host; struct sockaddr_in serv_addr; if(argc < 2){ fprintf(stderr,"Please enter the server's hostname!\n"); exit(1); } fprintf(stdout,"ip = %s\n",argv[1]); /*地址解析函数*/ if((host=gethostbyname(argv[1]))==NULL){ perror("gethostbyname"); exit(1); } /*创建 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(SERVPORT); 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,"hello",6,0))== -1){ perror("send"); exit(1); } printf("already send\n"); close(sockfd); }
10.4 ping源码分析
ping是基于ICMP协议的,ICMP是IP层的一个协议,它是用来探测主机、路由维护、路由选择和流量控制的。


浙公网安备 33010602011771号