嵌入式Linux网络编程
转载来自:http://blog.csdn.net/hongdongyu/article/details/8028194
一、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 <stdio.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <sys/un.h>
- #include <errno.h>
- #include <unistd.h>
- #include <signal.h>
- #include <sys/wait.h>
- #include <netdb.h>
- #include <pthread.h>
- //线程执行函数负责读写
- void *thr_fn(void *arg)
- {
- int size,j;
- char recv_buf[1024];
- int *parg=(int *)arg;
- int new_fd=*parg;
- printf("new_fd=%d\n",new_fd);
- while((size=read(new_fd,recv_buf,1024))>0)
- {
- if(recv_buf[0]=='@')
- break;
- printf("Message from client(%d): %s\n",size,recv_buf);
- for(j=0;j<size;j++)
- recv_buf[j]=toupper(recv_buf[j]);
- write(new_fd,recv_buf,size);
- }
- close(new_fd);
- return 0;
- }
- int main(int argc,char *argv[])
- {
- socklen_t clt_addr_len;
- int listen_fd;
- int com_fd;
- int ret;
- int i;
- static char recv_buf[1024];
- int len;
- int port;
- pthread_t tid;
- struct sockaddr_in clt_addr;
- struct sockaddr_in srv_addr;
- //服务器端运行时要给出端口信息,该端口为监听端口
- if(argc!=2)
- {
- printf("Usage:%s port\n",argv[0]);
- return 1;
- }
- //获得输入的端口
- port=atoi(argv[1]);
- //创建套接字用于服务器的监听
- listen_fd=socket(PF_INET,SOCK_STREAM,0);
- if(listen_fd<0)
- {
- perror("cannot create listening socket");
- return 1;
- }
- //填充关于服务器的套节字信息
- memset(&srv_addr,0,sizeof(srv_addr));
- srv_addr.sin_family=AF_INET;
- srv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
- srv_addr.sin_port=htons(port);
- //将服务器和套节字绑定
- ret=bind(listen_fd,(struct sockaddr *)&srv_addr,sizeof(srv_addr));
- if(ret==-1)
- {
- perror("cannot bind server socket");
- close(listen_fd);
- return 1;
- }
- //监听指定端口,连接5个客户端
- ret=listen(listen_fd,5);
- if(ret==-1)
- {
- perror("cannot listen the client connect request");
- close(listen_fd);
- return 1;
- }
- //对每个连接来的客户端创建一个线程,单独与其进行通信
- //首先调用read函数读取客户端发送来的信息
- //将其转换成大写后发送回客户端
- //当输入“@”时,程序退出
- while(1)
- {
- len=sizeof(clt_addr);
- com_fd=accept(listen_fd,(struct sockaddr *)&clt_addr,&len);
- if(com_fd<0)
- {
- if(errno==EINTR)
- {
- continue;
- }
- else
- {
- perror("cannot accept client connect request");
- close(listen_fd);
- return 1;
- }
- }
- printf("com_fd=%d\n",com_fd);//打印建立连接的客户端产生的套节字
- if((pthread_create(&tid,NULL,thr_fn,&com_fd))==-1)
- {
- perror("pthread_create error");
- close(listen_fd);
- close(com_fd);
- return 1;
- }
- }
- return 0;
- }
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <sys/un.h>
- #include <netdb.h>
- #include <unistd.h>
- 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\n",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\n",snd_buf);
- if(snd_buf[0]=='@')
- break;
- }
- close(connect_fd);
- return 0;
- }