嵌入式Linux网络编程

一、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\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

 

#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\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;
}

 

你可能感兴趣的:(linux)