注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

BCB-DG's Blog

...

 
 
 

日志

 
 

Linux网络编程入门三(转)  

2013-03-30 16:45:34|  分类: Linux |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
(七)Linux网络编程--7. TCP/IP协议

你也许听说过TCP/IP协议,那么你知道到底什么是TCP,什么是IP吗?在这一章里面,我们一起来学习这个目前网络上用最广泛的协议.

7.1 网络传输分层
    如果你考过计算机等级考试,那么你就应该已经知道了网络传输分层这个概念.在网络上,人们为了传输数据时的方便,
    把网络的传输分为7个层次.分别是:应用层,表示层,会话层,传输层,网络层,数据链路层和物理层.分好了层以后,传输数据时,
    上一层如果要数据的话,就可以直接向下一层要了,而不必要管数据传输的细节.下一层也只向它的上一层提供数据,
    而不要去管其它东西了.如果你不想考试,你没有必要去记这些东西的.只要知道是分层的,而且各层的作用不同.

7.2 IP协议
    IP协议是在网络层的协议.它主要完成数据包的发送作用. 下面这个表是IP4的数据包格式

0      4       8       16                      32
--------------------------------------------------
|版本   |首部长度|服务类型|    数据包总长       |
--------------------------------------------------
|    标识                 |DF |MF| 碎片偏移      |
--------------------------------------------------
|   生存时间    |  协议   |  首部较验和         |
------------------------------------------------
|               源IP地址                        |
------------------------------------------------
|               目的IP地址                      |
-------------------------------------------------
|               选项                            |
=================================================
|               数据                            |
-------------------------------------------------                       

下面我们看一看IP的结构定义

struct ip
  {
#if __BYTE_ORDER == __LITTLE_ENDIAN
        unsigned int ip_hl:4;           /* header length */
        unsigned int ip_v:4;            /* version */
#endif
#if __BYTE_ORDER == __BIG_ENDIAN
        unsigned int ip_v:4;            /* version */
        unsigned int ip_hl:4;           /* header length */
#endif
        u_int8_t ip_tos;                /* type of service */
        u_short ip_len;                 /* total length */
        u_short ip_id;                  /* identification */
        u_short ip_off;                 /* fragment offset field */
#define IP_RF 0x8000                    /* reserved fragment flag */
#define IP_DF 0x4000                    /* dont fragment flag */
#define IP_MF 0x2000                    /* more fragments flag */
#define IP_OFFMASK 0x1fff               /* mask for fragmenting bits */
        u_int8_t ip_ttl;                /* time to live */
        u_int8_t ip_p;                  /* protocol */
        u_short ip_sum;                 /* checksum */
        struct in_addr ip_src, ip_dst;  /* source and dest address */
  };

ip_vIP协议的版本号,这里是4,现在IPV6已经出来了

ip_hlIP包首部长度,这个值以4字节为单位.IP协议首部的固定长度为20个字节,如果IP包没有选项,那么这个值为5.

ip_tos服务类型,说明提供的优先权.

ip_len说明IP数据的长度.以字节为单位.

ip_id标识这个IP数据包.

ip_off碎片偏移,这和上面ID一起用来重组碎片的.

ip_ttl生存时间.没经过一个路由的时候减一,直到为0时被抛弃.

ip_p协议,表示创建这个IP数据包的高层协议.如TCP,UDP协议.

ip_sum首部校验和,提供对首部数据的校验.

ip_src,ip_dst发送者和接收者的IP地址

关于IP协议的详细情况,请参考 RFC791

7.3 ICMP协议
ICMP是消息控制协议,也处于网络层.在网络上传递IP数据包时,如果发生了错误,那么就会用ICMP协议来报告错误.

ICMP包的结构如下:

0              8               16                              32
---------------------------------------------------------------------
|       类型    |       代码    |       校验和                  |
--------------------------------------------------------------------
|               数据            |       数据                    |
--------------------------------------------------------------------

ICMP在中的定义是
struct icmphdr
{
  u_int8_t type;                /* message type */
  u_int8_t code;                /* type sub-code */
  u_int16_t checksum;
  union
  {
    struct
    {
      u_int16_t id;
      u_int16_t sequence;
    } echo;                     /* echo datagram */
    u_int32_t   gateway;        /* gateway address */
    struct
    {
      u_int16_t __unused;
      u_int16_t mtu;
    } frag;                     /* path mtu discovery */
  } un;
};

关于ICMP协议的详细情况可以查看 RFC792

7.4 UDP协议
UDP协议是建立在IP协议基础之上的,用在传输层的协议.UDP和IP协议一样是不可靠的数据报服务.UDP的头格式为:


0                      16                      32
---------------------------------------------------
|       UDP源端口       |       UDP目的端口     |
---------------------------------------------------
|       UDP数据报长度   |       UDP数据报校验   |
---------------------------------------------------

UDP结构在中的定义为:
struct udphdr {
  u_int16_t     source;
  u_int16_t     dest;
  u_int16_t     len;
  u_int16_t     check;
};

关于UDP协议的详细情况,请参考 RFC768
7.5 TCP
TCP协议也是建立在IP协议之上的,不过TCP协议是可靠的.按照顺序发送的.TCP的数据结构比前面的结构都要复杂.

0       4       8  10           16              24              32
-------------------------------------------------------------------
|               源端口          |               目的端口        |
-------------------------------------------------------------------
|                               序列号                          |
------------------------------------------------------------------
|                               确认号                          |
------------------------------------------------------------------
|        |            |U|A|P|S|F|                               |
|首部长度| 保留       |R|C|S|Y|I|       窗口                    |
|        |            |G|K|H|N|N|                               |
-----------------------------------------------------------------
|               校验和          |               紧急指针        |
-----------------------------------------------------------------
|                       选项                    |    填充字节   |
-----------------------------------------------------------------

TCP的结构在中定义为:
struct tcphdr
  {
    u_int16_t source;
    u_int16_t dest;
    u_int32_t seq;
    u_int32_t ack_seq;
#if __BYTE_ORDER == __LITTLE_ENDIAN
    u_int16_t res1:4;
    u_int16_t doff:4;
    u_int16_t fin:1;
    u_int16_t syn:1;
    u_int16_t rst:1;
    u_int16_t psh:1;
    u_int16_t ack:1;
    u_int16_t urg:1;
    u_int16_t res2:2;
#elif __BYTE_ORDER == __BIG_ENDIAN
    u_int16_t doff:4;
    u_int16_t res1:4;
    u_int16_t res2:2;
    u_int16_t urg:1;
    u_int16_t ack:1;
    u_int16_t psh:1;
    u_int16_t rst:1;
    u_int16_t syn:1;
    u_int16_t fin:1;
#endif
    u_int16_t window;
    u_int16_t check;
    u_int16_t urg_prt;
};      

source发送TCP数据的源端口
dest接受TCP数据的目的端口

seq标识该TCP所包含的数据字节的开始序列号

ack_seq确认序列号,表示接受方下一次接受的数据序列号.

doff数据首部长度.和IP协议一样,以4字节为单位.一般的时候为5

urg如果设置紧急数据指针,则该位为1

ack如果确认号正确,那么为1

psh如果设置为1,那么接收方收到数据后,立即交给上一层程序

rst为1的时候,表示请求重新连接

syn为1的时候,表示请求建立连接

fin为1的时候,表示亲戚关闭连接

window窗口,告诉接收者可以接收的大小

check对TCP数据进行较核

urg_ptr如果urg=1,那么指出紧急数据对于历史数据开始的序列号的偏移值

关于TCP协议的详细情况,请查看 RFC793


7.6 TCP连接的建立
TCP协议是一种可靠的连接,为了保证连接的可靠性,TCP的连接要分为几个步骤.我们把这个连接过程称为"三次握手".

下面我们从一个实例来分析建立连接的过程.

第一步客户机向服务器发送一个TCP数据包,表示请求建立连接. 为此,客户端将数据包的SYN位设置为1,
并且设置序列号seq=1000(我们假设为1000).

第二步服务器收到了数据包,并从SYN位为1知道这是一个建立请求的连接.于是服务器也向客户端发送一个TCP数据包.
因为是响应客户机的请求, 于是服务器设置ACK为1,sak_seq=1001(1000+1)同时设置自己的序列号.seq=2000(我们假设为2000).

第三步客户机收到了服务器的TCP,并从ACK为1和ack_seq=1001知道是从服务器来的确认信息.于是客户机也向服务器发送确认信息.
客户机设置ACK=1,和ack_seq=2001,seq=1001,发送给服务器.至此客户端完成连接.

最后一步服务器受到确认信息,也完成连接.

通过上面几个步骤,一个TCP连接就建立了.当然在建立过程中可能出现错误,不过TCP协议可以保证自己去处理错误的.


说一说其中的一种错误.
  听说过DOS吗?(可不是操作系统啊).今年春节的时候,美国的五大网站一起受到攻击.攻击者用的就是DOS(拒绝式服务)方式.
  概括的说一下原理.客户机先进行第一个步骤.服务器收到后,进行第二个步骤.按照正常的TCP连接,客户机应该进行第三个步骤.
  不过攻击者实际上并不进行第三个步骤.因为客户端在进行第一个步骤的时候,修改了自己的IP地址,就是说将一个实际上不存在的
  IP填充在自己IP 数据包的发送者的IP一栏.这样因为服务器发的IP地址没有人接收,所以服务端会收不到第三个步骤的确认信号,
  这样服务务端会在那边一直等待,直到超时.这样当有大量的客户发出请求后,服务端会有大量等待,直到所有的资源被用光,
  而不能再接收客户机的请求.这样当正常的用户向服务器发出请求时,由于没有了资源而不能成功.
  于是就出现了春节时所出现的情况.

(八)Linux网络编程--8. 套接字选项

有时候我们要控制套接字的行为(如修改缓冲区的大小),这个时候我们就要控制套接字的选项了.


8.1 getsockopt和setsockopt

int getsockopt(int sockfd,int level,int optname,void *optval,socklen_t *optlen)
int setsockopt(int sockfd,int level,int optname,const void *optval,socklen_t *optlen)

level指定控制套接字的层次.可以取三种值:
        1)SOL_SOCKET:通用套接字选项.
        2)IPPROTO_IP:IP选项.
        3)IPPROTO_TCP:TCP选项.
optname指定控制的方式(选项的名称),我们下面详细解释

optval获得或者是设置套接字选项.根据选项名称的数据类型进行转换


选项名称                说明                                    数据类型
========================================================================
                        SOL_SOCKET
------------------------------------------------------------------------
SO_BROADCAST            允许发送广播数据                        int
SO_DEBUG                允许调试                                int
SO_DONTROUTE            不查找路由                              int
SO_ERROR                获得套接字错误                          int
SO_KEEPALIVE            保持连接                                int
SO_LINGER               延迟关闭连接                            struct linger
SO_OOBINLINE            带外数据放入正常数据流                  int
SO_RCVBUF               接收缓冲区大小                          int
SO_SNDBUF               发送缓冲区大小                          int
SO_RCVLOWAT             接收缓冲区下限                          int
SO_SNDLOWAT             发送缓冲区下限                          int
SO_RCVTIMEO             接收超时                                struct timeval
SO_SNDTIMEO             发送超时                                struct timeval
SO_REUSERADDR           允许重用本地地址和端口                  int
SO_TYPE                 获得套接字类型                          int
SO_BSDCOMPAT            与BSD系统兼容                           int
==========================================================================
                        IPPROTO_IP
--------------------------------------------------------------------------
IP_HDRINCL              在数据包中包含IP首部                    int
IP_OPTINOS              IP首部选项                              int
IP_TOS                  服务类型
IP_TTL                  生存时间                                int
==========================================================================
                        IPPRO_TCP
--------------------------------------------------------------------------
TCP_MAXSEG              TCP最大数据段的大小                     int
TCP_NODELAY             不使用Nagle算法                         int
=========================================================================

关于这些选项的详细情况请查看 Linux Programmer's Manual

8.2 ioctl
ioctl可以控制所有的文件描述符的情况,这里介绍一下控制套接字的选项.

int ioctl(int fd,int req,...)

==========================================================================
                        ioctl的控制选项
--------------------------------------------------------------------------
SIOCATMARK              是否到达带外标记                        int
FIOASYNC                异步输入/输出标志                       int
FIONREAD                缓冲区可读的字节数                      int
==========================================================================

详细的选项请用 man ioctl_list 查看.

(九)Linux网络编程--9. 服务器模型

学习过《软件工程》吧.软件工程可是每一个程序员"必修"的课程啊.如果你没有学习过, 建议你去看一看. 在这一章里面,
我们一起来从软件工程的角度学习网络编程的思想.在我们写程序之前, 我们都应该从软件工程的角度规划好我们的软件,
这样我们开发软件的效率才会高. 在网络程序里面,一般的来说都是许多客户机对应一个服务器.为了处理客户机的请求,
对服务端的程序就提出了特殊的要求.我们学习一下目前最常用的服务器模型.

<一>循环服务器:循环服务器在同一个时刻只可以响应一个客户端的请求

<二>并发服务器:并发服务器在同一个时刻可以响应多个客户端的请求


9.1 循环服务器:UDP服务器
        UDP循环服务器的实现非常简单:UDP服务器每次从套接字上读取一个客户端的请求,处理, 然后将结果返回给客户机.
可以用下面的算法来实现.

   socket(...);
   bind(...);
   while(1)
    {
         recvfrom(...);
         process(...);
         sendto(...);
   }
因为UDP是非面向连接的,没有一个客户端可以老是占住服务端. 只要处理过程不是死循环, 服务器对于每一个客户机的请求总是能够满足.

9.2 循环服务器:TCP服务器
TCP循环服务器的实现也不难:TCP服务器接受一个客户端的连接,然后处理,完成了这个客户的所有请求后,断开连接.

算法如下:
        socket(...);
        bind(...);
        listen(...);
        while(1)
        {
                accept(...);
                while(1)
                {
                        read(...);
                        process(...);
                        write(...);
                }
                close(...);
        }

TCP循环服务器一次只能处理一个客户端的请求.只有在这个客户的所有请求都满足后, 服务器才可以继续后面的请求.
这样如果有一个客户端占住服务器不放时,其它的客户机都不能工作了.因此,TCP服务器一般很少用循环服务器模型的.

9.3 并发服务器:TCP服务器
        为了弥补循环TCP服务器的缺陷,人们又想出了并发服务器的模型. 并发服务器的思想是每一个客户机的请求并不由服务器
直接处理,而是服务器创建一个 子进程来处理.

算法如下:

  socket(...);
  bind(...);
  listen(...);
  while(1)
  {
        accept(...);
        if(fork(..)==0)
          {
              while(1)
               {        
                read(...);
                process(...);
                write(...);
               }
           close(...);
           exit(...);
          }
        close(...);
  }     

TCP并发服务器可以解决TCP循环服务器客户机独占服务器的情况. 不过也同时带来了一个不小的问题.为了响应客户机的请求,
服务器要创建子进程来处理. 而创建子进程是一种非常消耗资源的操作.

9.4 并发服务器:多路复用I/O
为了解决创建子进程带来的系统资源消耗,人们又想出了多路复用I/O模型.
首先介绍一个函数select

int select(int nfds,fd_set *readfds,fd_set *writefds,
                fd_set *except fds,struct timeval *timeout)
void FD_SET(int fd,fd_set *fdset)
void FD_CLR(int fd,fd_set *fdset)
void FD_ZERO(fd_set *fdset)
int FD_ISSET(int fd,fd_set *fdset)

一般的来说当我们在向文件读写时,进程有可能在读写出阻塞,直到一定的条件满足. 比如我们从一个套接字读数据时,可能缓冲区里面没有数据可读 (通信的对方还没有 发送数据过来),这个时候我们的读调用就会等待(阻塞)直到有数据可读.如果我们不 希望阻塞,我们的一个选择是用select系统调用. 只要我们设置好select的各个参数,那么当文件可以读写的时候select回"通知"我们 说可以读写了. readfds所有要读的文件文件描述符的集合
writefds所有要的写文件文件描述符的集合

exceptfds其他的服要向我们通知的文件描述符

timeout超时设置.

nfds所有我们监控的文件描述符中最大的那一个加1

在我们调用select时进程会一直阻塞直到以下的一种情况发生. 1)有文件可以读.2)有文件可以写.3)超时所设置的时间到.

为了设置文件描述符我们要使用几个宏. FD_SET将fd加入到fdset

FD_CLR将fd从fdset里面清除

FD_ZERO从fdset中清除所有的文件描述符

FD_ISSET判断fd是否在fdset集合中

使用select的一个例子

int use_select(int *readfd,int n)
{
   fd_set my_readfd;
   int maxfd;
   int i;
   
   maxfd=readfd[0];
   for(i=1;i
    if(readfd[i]>maxfd) maxfd=readfd[i];
   while(1)
   {
        /*   将所有的文件描述符加入   */
        FD_ZERO(&my_readfd);
        for(i=0;i
            FD_SET(readfd[i],*my_readfd);
        /*     进程阻塞                 */
        select(maxfd+1,& my_readfd,NULL,NULL,NULL);
        /*        有东西可以读了       */
        for(i=0;i
          if(FD_ISSET(readfd[i],&my_readfd))
              {
                  /* 原来是我可以读了  */
                        we_read(readfd[i]);
              }
   }
}

使用select后我们的服务器程序就变成了.


        初始话(socket,bind,listen);
        
    while(1)
        {
        设置监听读写文件描述符(FD_*);   
        
        调用select;
        
        如果是倾听套接字就绪,说明一个新的连接请求建立
             {
                建立连接(accept);
                加入到监听文件描述符中去;
             }
       否则说明是一个已经连接过的描述符
                {
                    进行操作(read或者write);
                 }
                        
        }               

多路复用I/O可以解决资源限制的问题.这模型实际上是将UDP循环模型用在了TCP上面. 这也就带来了一些问题.
如由于服务器依次处理客户的请求,所以可能会导致有的客户 会等待很久.

9.5 并发服务器:UDP服务器
人们把并发的概念用于UDP就得到了并发UDP服务器模型. 并发UDP服务器模型其实是简单的.和并发的TCP服务器模型一样是创建
一个子进程来处理的 算法和并发的TCP模型一样.
除非服务器在处理客户端的请求所用的时间比较长以外,人们实际上很少用这种模型.


9.6 一个并发TCP服务器实例

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <arpa/inet.h>

#define MY_PORT         8888

int main(int argc ,char **argv)
{
int listen_fd,accept_fd;
struct sockaddr_in     client_addr;
int n;

if((listen_fd=socket(AF_INET,SOCK_STREAM,0))<0)
  {
        printf("Socket Error:%s/n/a",strerror(errno));
        exit(1);
  }

bzero(&client_addr,sizeof(struct sockaddr_in));
client_addr.sin_family=AF_INET;
client_addr.sin_port=htons(MY_PORT);
client_addr.sin_addr.s_addr=htonl(INADDR_ANY);
n=1;
/* 如果服务器终止后,服务器可以第二次快速启动而不用等待一段时间  */
setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&n,sizeof(int));
if(bind(listen_fd,(struct sockaddr *)&client_addr,sizeof(client_addr))<0)
  {
        printf("Bind Error:%s/n/a",strerror(errno));
        exit(1);
  }
  listen(listen_fd,5);
  while(1)
  {
   accept_fd=accept(listen_fd,NULL,NULL);
   if((accept_fd<0)&&(errno==EINTR))
          continue;
   else if(accept_fd<0)
    {
        printf("Accept Error:%s/n/a",strerror(errno));
        continue;
    }
  if((n=fork())==0)
   {
        /* 子进程处理客户端的连接 */
        char buffer[1024];

        close(listen_fd);
        n=read(accept_fd,buffer,1024);
        write(accept_fd,buffer,n);
        close(accept_fd);
        exit(0);
   }
   else if(n<0)
        printf("Fork Error:%s/n/a",strerror(errno));
   close(accept_fd);
  }
}

你可以用我们前面写客户端程序来调试着程序,或者是用来telnet调试
  评论这张
 
阅读(1210)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017