给什么平台,跳什么舞

第十章 嵌入式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层的一个协议,它是用来探测主机、路由维护、路由选择和流量控制的。

 

 

posted @ 2013-01-09 18:13  Jimwind  阅读(1782)  评论(0)    收藏  举报
==============精通*学习*关注==============