嵌入式Linux网络编程

2019-07-12 18:11发布

一、TCP/IP协议

1、TCP/IP参考模型

TCP/IP协议模型遵循简单明确的设计思路,包括以下四层协议:
  • 网络接口层:负责将二进制流转换为数据帧,并进行数据帧的发送和接受。数据帧是独立的网络信息传输单元。
  • 网络层:负责将数据帧封装成IP数据包,并运行必要的路由算法。
  • 传输层:负责端对端之间的通信会话连接与建立。传输协议的选择根据数据传输方式而定。
  • 应用层:负责应用程序的网络访问,通过端口号来识别各个不同的进程。

2、TCP和UDP

1)TCP向相邻的高层提供服务。因为TCP上一层是应用层,因此,TCP数据传输实现从一个应用程序到另一个应用程序的数据传递。应用程序通过编程调用TCP并使用TCP服务,提供需要准备发送的数据,来区分接受数据应用的目的地址和端口号。它允许数据同网络上的其他节点进行可靠的交换,它能提供端口编号的译码,以识别主机的应用程序,而且完成数据的可靠传输。TCP协议具有严格的内装差错检验算法确保数据的完整性,它是面向字节的顺序协议,这意味着包内的每个字节被分配一个顺序编号,并分配给每个包一个顺序编号。 2)UDP协议是一种无连接协议,不需要像TCP一样建立连接,一个UDP应用可同时作为应用的客户或服务器方。当接收数据时它不向发送方提供确认信息,如果出现丢失或重复的情况,也不会向发送方发出差错报文。由于它执行功能时具有较低的开销,因而执行速度比TCP快。

3、协议的选择

1)对数据要求高可靠性的应用选择TCP协议,如验证、密码字段的传送都是不许出错的,而对数据可靠性要求不那么高的可选择UDP传送。 2)TCP的传送会有较大的延迟,不适合对实时性要求较高的应用,如VOIP、视频监控等。相反,UDP协议则在这些应用中发挥很好的作用。 3)TCP协议主要解决网络的可靠性问题,它通过各种机制减少错误发生的概率。因此,在网络状况不是很好的情况下用TCP协议,但是若在网络状况很好的情况下就不需要采用TCP协议,而是选择UDP协议来减少网络负荷。

二、网络基础编程

1、socket概述

Linux中的网络编程是通过socket接口来进行的,它也是一种文件描述符。通过它不仅可以在本地机器上实现进程间的通信,而且通过网络能够在不同的机器上的进程之间进行通信。socket也有一个类似打开文件的函数调用,该函数返回一个整型的socket描述符,随后的连接建立等操作都是通过socket来实现的。 socket类型常见有以下三种:
  • 流式socket(SOCK_STREAM):流式套接字提供可靠的、面向连接的通信流;它使用TCP协议,保证了数据的正确性和顺序性。
  • 数据报socket(SOCK_DGRAM):数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,而且不保证是可靠、无差错的。使用数据报协议UDP。
  • 原始socket:允许对底层协议进行直接访问,功能强大但使用不便,主要用于一些协议的开发。

2、地址及顺序处理

1)地址结构相关处理

一种常用的用于保存网络地址的数据结构sockaddr_in,其结构如下: struct sockaddr_in { short int sin_family; /*地址族*/ unsigned short int sin_port; /*端口号*/ struct in_addr sin_addr; /*IP地址*/ unsigned char sin_zero[8]; /*填充0*/ }; 该结构sin_family字段可选常见值: AF_INET:IPv4协议 AF_INET6:IPv6协议 AF_LOCAL:UNIX域协议 AF_LINK:链路地址协议 AF_KEY:密钥套接字

2)数据存储优先顺序

计算机数据存储有两种字节优先顺序:高位字节优先(大端模式)和低位字节优先(小段模式)。Internet上以高位字节优先的顺序在网络传输,而PC机通常采用小端模式,因此有时候需要对两个字节存储优先顺序进行转换。用到了4个函数:htons()、ntohs()、htonl()和ntohl()。h代表host,n代表network,s代表short,l代表long。通常16位的IP端口号用s,而IP地址用l。

3)地址格式转换

IP地址通常由数字加点(192.168.0.1)的形式表示,而在struct in_addr中使用的IP地址是由32位整数表示,为了转换可以使用下面三个函数: int inet_aton(const char *cp,struct in_addr *inp); char *inet_ntoa(struct in_addr in); in_addr_t inet_addr(const char *cp); 其中inet_aton将a.b.c.d形式的IP转换为32位的IP,存储在inp指针里面;inet_ntoa是将32位IP转换为a.b.c.d的格式;inet_addr将一个点分十进制的IP转换成一个长整数型数。

4)名字地址转换

通常,人们在使用过程中不愿记忆冗长的IP地址,因此,使用主机名是很好的选择。gethostbyname()将主机名转化为IP地址,gethostbyaddr()则是逆操作,将IP地址转换为主机名。它们都涉及到一个hostent的结构体,如下: struct hostent { char *h_name; /*正式主机名*/ char **h_aliases; /*主机别名*/ int h_addrtype; /*地址类型*/ int h_length; /*地址字节长度*/ char **h_addr_list; /*指向IPv4或IPv6的地址指针数组*/ };  我们调用gethostbyname()或者gethostbyaddr()后就能返回hostent结构体的相关信息。

三、socket基础编程

1、相关函数介绍

socket编程的基本函数有socket()、bind()、listen()、accept()、sent()、sendto()、recv()、以及recvfrom()等,具体介绍如下:
  • socket():用于建立一个socket连接,可指定socket类型等信息。建立之后,可对sockaddr或sockaddr_in结果进行初始化,以保存所建立的socket地址信息。
  •  bind():用于将本地IP地址绑定到端口号。
  • listen():在服务程序成功建立套接字和地址进行绑定后,调用listen()函数来创建一个等待队列,在其中存放未处理的客户端连接请求。
  • accept():服务器调用listen()创建等待队列之后,调用accept()等待并接收客户端的连接请求。通常从由bind()所创建的等待队列中取出第一个未处理的连接请求。
  • connect():用于bind()之后的client端,用于与服务器端建立连接。
  • send()和recv():这两个函数分别用于发送和接收数据,可以用在TCP或者UDP中。用在UDP时可以在connect()建立连接之后再用。
 
  • sendto()和recvfrom():作用与前两个类似,当用在TCP时,后面的几个与地址有关参数不起作用,等同于send()、recv();用在UDP时,可用在之前没有使用connect的情况下,这两个函数可自动寻找指定地址并进行连接。

2、流程及流程图

基于TCP-服务器:创建socket()—>bind()绑定IP地址、端口信息到socket上—>listen()设置允许最大连接数—>accept()等待来自客户端的连接请求—>send()、recv()或者read()、write()收发数据—>关闭连接。 基于TCP-客户端:创建socket()—>设置要连接的服务器IP地址和端口等属性—>connect()连接服务器—>send()、recv()或read()、write()收发数据—>关闭网络连接。 基于UDP-服务器:创建socket()—>bind()绑定IP地址、端口等信息到socket上—>循环接受数据,用recvfrom()—>关闭网络连接。 基于UDP-客户端:创建socket()—>bind()绑定IP地址、端口等信息到socket上—>设置对方IP地址和端口信息—>sendto()发送数据—>关闭网络连接。

四、服务器类型

循环服务器:服务器在同一时间只能响应一个客户端的请求。 并发服务器:服务器在同一时刻可以响应多个客户端的请求。 UDP循环服务器 socket(...); bind(...); while(1) { recvfrom(...); process(...); sendto(...); } TCP循环服务器 socket(...); bind(...); listen(...); while(1) { accept(...); process(...); close(...); } TCP循环服务器一次只能处理一个客户端的请求,只有这个客户的所有请求都满足后,才可以继续后面的请求。这样如果一个客户端占住服务器不放,其他的客户都不能工作,所以TCP服务器一般很少用循环服务器模型。而UDP循环服务器可以同时相应多个客户端的请求。 TCP并发服务器 socket(...); bind(...); listen(...); while(1) { accept(...); if(fork()==0) { process(...); close(...); exit(...); } close(...); } 并发服务器的思想是每一个客户端的请求并不由服务器直接处理,而是有服务器创建一个子进程或者线程来处理。

五、使用实例

/*server_thread.c*/ #include #include #include #include #include #include #include #include #include #include //线程执行函数负责读写 void *thr_fn(void *arg) { int size,j; char recv_buf[1024]; int *parg=(int *)arg; int new_fd=*parg; printf("new_fd=%d ",new_fd); while((size=read(new_fd,recv_buf,1024))>0) { if(recv_buf[0]=='@') break; printf("Message from client(%d): %s ",size,recv_buf); for(j=0;j   #include #include #include #include #include #include int main(int argc,char *argv[]) { int connect_fd; int ret; char snd_buf[1024]; int i; int port; int len; static struct sockaddr_in srv_addr; //客户端运行需要给出具体的连接地址和端口 if(argc!=3) { printf("Usage: %s server_ip_address port ",argv[0]); return 1; } //获得输入的端口 port=atoi(argv[2]); //创建套节字用于客户端的连接 connect_fd=socket(PF_INET,SOCK_STREAM,0); if(connect_fd<0) { perror("cannot create communication socket"); return 1; } //填充关于服务器的套节字信息 memset(&srv_addr,0,sizeof(srv_addr)); srv_addr.sin_family=AF_INET; srv_addr.sin_addr.s_addr=inet_addr(argv[1]); srv_addr.sin_port=htons(port); //连接指定的服务器 ret=connect(connect_fd,(struct sockaddr *)&srv_addr,sizeof(srv_addr)); if(ret==-1) { perror("cannot connect to the server"); close(connect_fd); return 1; } memset(snd_buf,0,1024); //用户输入信息后,程序将输入的信息通过套接字发送给服务器 //然后调用read函数从服务器中读取发送来的信息 //当输入“@”时,程序退出 while(1) { write(STDOUT_FILENO,"input message:",14); len=read(STDIN_FILENO,snd_buf,1024); if(len>0) write(connect_fd,snd_buf,len); len=read(connect_fd,snd_buf,len); if(len>0) printf("Message form server: %s ",snd_buf); if(snd_buf[0]=='@') break; } close(connect_fd); return 0; }