嵌入式网络编程

在Linux中的网络编程是通过socket接口来进行的。是一种文件描述符。socket也有一个类似于打开文件的函数调用,该函数返回一个整型的socket描述符,随后的连接建立、数据传输等操作都是通过socket来实现的。
常见的socket有3种类型:

(1)流式socket (SOCK_STREAM) 流式套接字提供可靠的、面向连接的通信流;它使用TCP协议,从而保证了数据传输的正确性和顺序性。

(2)数据报socket(SOCK_DGRAM) 数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议UDP

(3)原始socket 原始套接字允许对底层协议如IP或ICMP进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。

1:sockaddr/_in:是用来 保存 socket 信息的 .在 建立 socketadd 或 sockaddr_in 后,就可以对该 socket 进行适当的操作了.

struct sockaddr { 
unsigned short sa_family; /*地址族*/ 
char sa_data[14]; /*14 字节的协议地址,包含该 socket 的 IP 地址和端口号。*/ 
}; 
struct sockaddr_in { 
short int sin_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:密钥套接字(socket)

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

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数据

地址格式转化

IP地址通常由数字加点(192.168.0.1)的形式表示,而在struct in_addr中使用的IP地址是由32位整数表示,为了转换可以使用下面三个函数:

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

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转换成一个长整数型数。

名字地址转换
通常,人们在使用过程中不愿记忆冗长的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结构体的相关信息。

3.socket编程的基本函数有socket()、bind()、listen()、accept()、sent()、sendto()、recv()、以及recvfrom()等,具体介绍如下:
这里写图片描述
基于TCP-服务器:创建socket()—>bind()绑定IP地址、端口信息到socket上—>listen()设置允许最大连接数—>accept()等待来自客户端的连接请求—>send()、recv()或者read()、write()收发数据—>关闭连接。
基于TCP-客户端:创建socket()—>设置要连接的服务器IP地址和端口等属性—>connect()连接服务器—>send()、recv()或read()、write()收发数据—>关闭网络连接。

循环服务器:服务器在同一时间只能响应一个客户端的请求。

socket(...);  
bind(...);  
listen(...);  
while(1)  
{  
   accept(...);  
   process(...);  
   close(...);  
}  

3.1. socket:该函数用于建立一个 socket 连接,可指定 socket 类型等信息。在建立了 socket连接之后,可对 socketaddr 或 sockaddr_in 进行初始化,以保存所建立的 socket 信息。

sockfd = socket(AF_INET,SOCK_STREAM,0))

int socket(int family, int type, int protocol) 函数原型。family就是上面那几种,
type:
SOCK_STREAM:字节流套接字socket
SOCK_DGRAM:数据报套接字 socket
SOCK_RAW:原始套接字 socket 。

protoco:0(原始套接字除外)

成功:非负套接字描述符
出错:−1

调用sockfd函数后,建立了一个socket连接,接着就对sockaddr进行初始化。
server_sockaddr.sin_family=AF_INET; //使用ipv4协议
server_sockaddr.sin_port=htons(SERVPORT);// 设置服务器监听端口号,若直接为零,则系统可随机选择未被使用的端口号
server_sockaddr.sin_addr.s_addr=INADDR_ANY; //系统会自动填入本机ip地址
bzero(&(server_sockaddr.sin_zero),8);这个是把剩余位置零,匹配另一种结构体

3.2. bind:该函数是用于将本地 IP 地址绑定端口号的,若绑定其他地址则不能成功。另外,它主要用于 TCP 的连接,而在 UDP 的连接中则无必要。

bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr))

int bind(int sockfd,struct sockaddr *my_addr, int addrlen)
sockfd:套接字描述符
my_addr:本地sockaddr地址信息,转化成sockaddr格式了,原来是in格式的,in赋值简单。
addrlen:地址长度
端口号和地址在 my_addr 中给出了,若不指定地址,则内核随意分配一个临时端口给该 应用程序。
成功为0,否则-1

3.3. listen():在服务程序成功建立套接字和地址进行绑定后,调用listen()函数来创建一个等待队列,在其中存放未处理的客户端连接请求。

listen(sockfd,BACKLOG)

listen函数把一个未连接的套接口转换成一个被动套接口,指示内核应接受指向该套接口的连接请求。根据TCP状态转换图,调用listen导致套接口从CLOSED状态转换到LISTEN状态。

本函数的第二个参数规定了内核应该为相应套接口排队的最大连接个数。
为了更好的理解backlog参数,我们必须认识到内核为任何一个给定的监听套接口维护两个队列:
1、未完成连接队列(incomplete connection queue),每个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接口处于SYN_RCVD状态。
2、已完成连接队列(completed connection queue),每个已完成TCP三路握手过程的客户对应其中一项。这些套接口处于ESTABLISHED状态。
现在backlog用来确定已完成队列(完成三次握手等待accept)的长度,而不再是已完成队列和未完成连接队列之和。未完成队列(incomplete connection queue)的长度现在由/proc/sys/net/ipv4/tcp_max_syn_backlog设置。
为了接受连接,先用socket()创建一个套接口的描述字,然后用listen()创建套接口并为申请进入的连接建立一个后备日志,然后便可用accept()接受连接了。listen()仅适用于支持连接的套接口,如SOCK_STREAM类型的。
int PASCAL FAR listen( SOCKET s, int backlog);
S:用于标识一个已捆绑未连接套接口的描述字。
backlog:等待连接队列的最大长度,默认缺省为5。
成功为0,否则-1
3.4 accept():服务器调用listen()创建等待队列之后,调用accept()等待并接收客户端的连接请求。通常从由bind()所创建的等待队列中取出第一个未处理的连接请求。

client_fd=accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size)

int accept(int sockfd, struct sockaddr *addr(客户端地址), socklen_t *addrlen(地址长度))
实际上是这样的: accept函数指定服务端去接受客户端的连接,接收后,返回了客户端套接字的标识,且获得了客户端套接字的“地方”(包括客户端IP和端口信息等)。你调用 accept() 告诉它你有空闲的连接。它将返回一个新的套接字文 件描述符!这样你就有两个套接字了,原来的一个还在侦听你的那个端口, 新的在准备发送 (send()) 和接收 ( recv()) 数据。这就是这个过程!在系统调用 send() 和 recv() 中你应该使用新的套接字描述符 new_fd。如果你只想让一个连接进来,那么你可以使用 close() 去关闭原 来的文件描述符 sockfd 来避免同一个端口更多的连接。
accept函数非常地痴情,痴心不改:如果没有客户端套接字去请求,它便会在那里一直痴痴地等下去,直到永远(阻塞式)。当你第一次调用 socket() 建立套接口描述符的时候,内核就将他设置为阻塞。如果你不想套接口阻塞,你就要调用函数 fcntl():通过设置套接口为非阻塞,你能够有效地”询问”套接口以获得信息,但是一般来说轮询不是一个好主意,会浪费cpu时间,更好的方法是用 select()方法 去查询是否有数据要读进来select()–多路同步 I/O
select() 让你可以同时监视多个套接口。如果你想知道的话,那么他就会告诉你哪个套接口准备读,哪个又准备好了写,哪个套接口又发生了例外 (exception)。
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
成功为0,否则-1

3.5. connect:该函数在TCP中是用于 bind 的之后的 client 端,用于与服务器端建立连接,而在 UDP 中由于没有了 bind 函数,因此用 connect 有点类似 bind 函数的作用。

client_fd=accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size)

int connect(int sockfd, struct sockaddr *serv_addr(服务器端地址), int addrlen)
用来将参数sockfd 的socket 连至参数serv_addr 指定的网络地址。connect函数将使用参数sockfd中的套接字连接到参数serv_addr中指定的服务器
参数一:套接字描述符
参数二:指向数据结构sockaddr的指针,其中包括目的端口和IP地址
参数三:参数二sockaddr的长度,可以通过sizeof(struct sockaddr)获得
成功则返回0,失败返回非0,错误码GetLastError()。

其中socket没有什么可疑问的,主要是创建一个套接字用于与服务端交换数据,并且通常它 会迅速返回,此时并没有数据通过网卡发送出去,而紧随其后的connect函数则会产生网络数据的发送,TCP的三次握手也正是在此时开 始,connect会先发送一个SYN包给服务端,并从最初始的CLOSED状态进入到SYN_SENT状态,在此状态等待服务端的确认包,通常情况下这 个确认包会很快到达,以致于我们根本无法使用netstat命令看到SYN_SENT状态的存在,不过我们可以做一个极端情况的模拟,让客户端去连接一个 随意指定服务器(如IP地址为88.88.88.88),因为该服务器很明显不会反馈给我们SYN包的确认包(SYN ACK),客户端就会在一定时间内处于SYN_SENT状态,并在预定的超时时间(比如3分钟)之后从connect函数返回,connect调用一旦失 败(没能到达ESTABLISHED状态)这个套接字便不可用,若要再次调用connect函数则必须要重新使用socket函数创建新的套接字。
至此。算是连上了,接下来就要传输数据进行握手了。

3.6. send()和recv():这两个函数分别用于发送和接收数据,可以用在TCP或者UDP中。用在UDP时可以在connect()建立连接之后再用。

sendbytes=send(sockfd,"hello",5,0)
recvbytes=recv(client_fd,buf,MAXDATASIZE,0)

int send(int sockfd, const void *msg, int len, int flags)
int recv(int sockfd, void *buf, int len, unsigned int flags)
msg指向要发送内容的指针,len发送内容长度,flags一般为0
buf指向存放接受数据的缓冲区。

ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
1) send先比较发送数据的长度nbytes和套接字sockfd的发送缓冲区的长度,如果nbytes > 套接字sockfd的发送缓冲区的长度, 该函数返回SOCKET_ERROR;
2) 如果nbtyes <= 套接字sockfd的发送缓冲区的长度,那么send先检查协议是否正在发送sockfd的发送缓冲区中的数据,如果是就等待协议把数据发送完,如果协议 还没有开始发送sockfd的发送缓冲区中的数据或者sockfd的发送缓冲区中没有数据,那么send就比较sockfd的发送缓冲区的剩余空间和 nbytes
3) 如果 nbytes > 套接字sockfd的发送缓冲区剩余空间的长度,send就一起等待协议把套接字sockfd的发送缓冲区中的数据发送完
4) 如果 nbytes < 套接字sockfd的发送缓冲区剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把套接字sockfd的发送缓冲 区中的数据传到连接的另一端的,而是协议传送的,send仅仅是把buf中的数据copy到套接字sockfd的发送缓冲区的剩余空间里)。
5) 如果send函数copy成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR; 如果在等待协议传送数据时网络断开,send函数也返回SOCKET_ERROR。
6) send函数把buff中的数据成功copy到sockfd的改善缓冲区的剩余空间后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果 协议在后续的传送过程中出现网络错误的话,那么下一个socket函数就会返回SOCKET_ERROR。(每一个除send的socket函数在执行的 最开始总要先等待套接字的发送缓冲区中的数据被协议传递完毕才能继续,如果在等待时出现网络错误那么该socket函数就返回SOCKET_ERROR)
7) 在unix系统下,如果send在等待协议传送数据时网络断开,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的处理是进程终止。

3.7. sendto()和recvfrom():作用与前两个类似,当用在TCP时,后面的几个与地址有关参数不起作用,等同于send()、recv();用在UDP时,可用在之前没有使用connect的情况下,这两个函数可自动寻找指定地址并进行连接。

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.8 close(sockfd);关闭套接字。

**4.**UDP传输模式
这里写图片描述

基于UDP-服务器:创建socket()—>bind()绑定IP地址、端口等信息到socket上—>循环接受数据,用recvfrom()—>关闭网络连接。
基于UDP-客户端:创建socket()—>bind()绑定IP地址、端口等信息到socket上—>设置对方IP地址和端口信息—>sendto()发送数据—>关闭网络连接。

5:服务器类型
循环服务器:
TCP循环服务器一次只能处理一个客户端的请求,只有这个客户的所有请求都满足后,才可以继续后面的请求。这样如果一个客户端占住服务器不放,其他的客户都不能工作,所以TCP服务器一般很少用循环服务器模型。而UDP循环服务器可以同时相应多个客户端的请求。

UDP循环服务器

socket(...);  
bind(...);  
while(1)  
{  
   recvfrom(...);  
   process(...);  
   sendto(...);  
}  

TCP循环服务器

socket(...);  
bind(...);  
listen(...);  
while(1)  
{  
   accept(...);  
   process(...);  
   close(...);  
}  

并发服务器:服务器在同一时刻可以响应多个客户端的请求。
TCP并发服务器
并发服务器的思想是每一个客户端的请求并不由服务器直接处理,而是有服务器创建一个子进程或者线程来处理。

socket(...);  
bind(...);  
listen(...);  
while(1)  
{  
   accept(...);  
   /*fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
   fork函数总是“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。fork在子进程中的返回值是0,而在父进程中的返回值则是子进程的id。 
   子进程在创建的时候会复制父进程的当前状态*/
   if(fork()==0)  
     {  
        process(...);  
        close(...);  
        exit(...);  
     }  
     close(...);  
}  

6.在实际情况中,人们往往遇到多个客户端连接服务端的情况。由于之前介绍的如connet、recv、send都是阻塞性函数,若资源没有准备好,则调用该函数的进程将进入睡眠状态,这样就无法处理I/O多路复用的情况了。由于在Linux中把socket也作为一种特殊文件描述符,这给用户的处理带来了很大方便。
6.1:fcntl可以改变已打开文件的属性。
int fcntl(int fd,int cmd,….int arg)
第三个参数可以是一个整数,也可以是指向一个结构的指针flock;
fd是文件描述符,第二个参数是CMD

在基于套接字的异步I/O(当一个描述符已准备好,可以启动I/O时,进程会通知内核。)中,当从套接字中读取数据时,或者当套接字写队列中空间变得可用时,可以安排要发送的i信号SIGIO。启动异步I/O的步骤:
1)建立套接字所有权,这样信号就可以被传递到合适的进程。
在fcntl中使用F_SETOWN命令 或
在fcntl中使用FIOSETOWN命令 或
在fcntl中使用FIOCSPGRP命令
2)通知套接字当I/O操作不会阻塞时发信号。
在fcntl中使用F_SETFL命令并且启用文件标志O_ASYNC
在ioctl中使用FIOASYNC命令。

/*调fcntl 函数设置非阻塞参数*/ 
if((flags=fcntl( sockfd, F_SETFL, 0))<0) //先清除标志
perror("fcntl F_SETFL"); 
flag |= O_NONBLOCK; 
if(fcntl(fd,F_SETEL,flags)<0) //设置非阻塞I/O
perror("fcntl"); 
//在server中listen后加入后,accept资源不可用时,系统就会返回。

6.2 使用fcntl函数虽然可以实现非阻塞I/O或信号驱动I/O,但在实际使用时往往会对资源是否准备完毕进行循环测试,这样就大大增加了不必要的CPU资源。
在这里可以使用select函数来解决这个问题,同时,使用select函数还可以设置等待时间,可以说功能更加强大。

Int select(int maxfdpl,fd_set *restrict readfds, fd_set *restrict writefds,fd_set *restrict exceptfds,  
struct timeval *restrict tvptr)
/*返回值:准备就绪的描述符数目;若超时,返回0,若出错返回-1
第一个参数是最大文件描述符编号值加1,这的目的是指定我们关注的最大描述符,内核就在此范围内寻找打开的位。
第234参数分别代表读集,写集,异常集。
对读集的一个描述符进行read不会阻塞,则表示准备好了
对写集的一个描述符进行write不会阻塞,则表示准备好了
若描述符有一个未决异常,则异常集准备好了
最后一个参数是tv_sec和tv_unsec可设置等待时间。微妙集*/

FD_ZERO(fd_set *set) 清除一个文件描述符集
FD_SET(int fd,fd_set *set)将一个文件描述符加入集中
FD_CLR(int fd,fd_set *set)清除一个文件描述符从集中
FD_ISSET(int fd,fd_set *set)测试集中一个给定文件描述符是否有变化

select函数主要就是要把这些文件描述符添加进各种集,通过返回值可知道3个描述符集中已准备好的描述符数之和,接着就用FD_ISSET函数测试该集中的一个给定文件描述符是否处于打开状态。

程序编写步骤:
FD_ZERO(&set);
FD_SET(fd,&set);
FD_SET(STDIN_FILENO,&set);
select;
if(FD_ISSET(fd,&set)){}
也是在listen后运行,accept后的代码都在大括号内。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章