Chinaunix首页 | 论坛 | 博客
  • 博客访问: 74586
  • 博文数量: 41
  • 博客积分: 1665
  • 博客等级: 上尉
  • 技术积分: 410
  • 用 户 组: 普通用户
  • 注册时间: 2006-05-04 05:31
文章分类

全部博文(41)

文章存档

2018年(1)

2011年(1)

2006年(39)

我的朋友

分类: LINUX

2006-05-11 12:16:16

网络编程(6)

 

6. 高级套接字函数

在前面的几个部分里面,我们已经学会了怎么样从网络上读写信息了.前面的一些函数(r

ead,write)是网络程序里面最基本的函数.也是最原始的通信函数.在这一章里面,我们一

起来学习网络通信的高级函数.这一章我们学习另外几个读写函数.

6.1 recv和send

recv和send函数提供了和read和write差不多的功能.不过它们提供 了第四个参数来控制

读写操作.

int recv(int sockfd,void *buf,int len,int flags)

int send(int sockfd,void *buf,int len,int flags)

前面的三个参数和read,write一样,第四个参数可以是0或者是以下的组合

_______________________________________________________________

| MSG_DONTROUTE | 不查找路由表 |

| MSG_OOB | 接受或者发送带外数据 |

| MSG_PEEK | 查看数据,并不从系统缓冲区移走数据 |

| MSG_WAITALL | 等待所有数据 |

|--------------------------------------------------------------|

MSG_DONTROUTE:是send函数使用的标志.这个标志告诉IP协议.目的主机在本地网络上面

,没有必要查找路由表.这个标志一般用网络诊断和路由程序里面.

MSG_OOB:表示可以接收和发送带外的数据.关于带外数据我们以后会解释的.

MSG_PEEK:是recv函数的使用标志,表示只是从系统缓冲区中读取内容,而不清楚系统缓冲

区的内容.这样下次读的时候,仍然是一样的内容.一般在有多个进程读写数据时可以使用

这个标志.

MSG_WAITALL是recv函数的使用标志,表示等到所有的信息到达时才返回.使用这个标志的

时候recv回一直阻塞,直到指定的条件满足,或者是发生了错误. 1)当读到了指定的字节

,函数正常返回.返回值等于len 2)当读到了文件的结尾时,函数正常返回.返回值小于

len 3)当操作发生错误时,返回-1,且设置错误为相应的错误号(errno)

如果flags为0,则和read,write一样的操作.还有其它的几个选项,不过我们实际上用的很

,可以查看 Linux Programmer's Manual得到详细解释.

6.2 recvfrom和sendto

这两个函数一般用在非套接字的网络程序当中(UDP),我们已经在前面学会了.

6.3 recvmsg和sendmsg

recvmsg和sendmsg可以实现前面所有的读写函数的功能.

int recvmsg(int sockfd,struct msghdr *msg,int flags)

int sendmsg(int sockfd,struct msghdr *msg,int flags)

struct msghdr

{

void *msg_name;

int msg_namelen;

struct iovec *msg_iov;

int msg_iovlen;

void *msg_control;

int msg_controllen;

int msg_flags;

}

struct iovec

{

void *iov_base; /* 缓冲区开始的地址 */

size_t iov_len; /* 缓冲区的长度 */

}

msg_name和 msg_namelen当套接字是非面向连接时(UDP),它们存储接收和发送方的地址

信息.msg_name实际上是一个指向struct sockaddr的指针,msg_name是结构的长度.当套

接字是面向连接时,这两个值应设为NULL. msg_iov和msg_iovlen指出接受和发送的缓冲

区内容.msg_iov是一个结构指针,msg_iovlen指出这个结构数组的大小. msg_control和

msg_controllen这两个变量是用来接收和发送控制数据时的 msg_flags指定接受和发送

的操作选项.和recv,send的选项一样

6.4 套接字的关闭

关闭套接字有两个函数close和shutdown.用close时和我们关闭文件一样.

6.5 shutdown

int shutdown(int sockfd,int howto)

TCP连接是双向的(是可读写的),当我们使用close时,会把读写通道都关闭,有时侯我们希

望只关闭一个方向,这个时候我们可以使用shutdown.针对不同的howto,系统回采取不同

的关闭方式.

howto=0这个时候系统会关闭读通道.但是可以继续往接字描述符写.

howto=1关闭写通道,和上面相反,着时候就只可以读了.

howto=2关闭读写通道,和close一样 在多进程程序里面,如果有几个子进程共享一个套接

字时,如果我们使用shutdown, 那么所有的子进程都不能够操作了,这个时候我们只能够

使用close来关闭子进程的套接字描述符.

 

 

 

网络编程(7)

 

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_se

q=1001(1000+1)同时设置自己的序列号.seq=2000(我们假设为2000).

第三步客户机收到了服务器的TCP,并从ACK为1和ack_seq=1001知道是从服务器来的确认

信息.于是客户机也向服务器发送确认信息.客户机设置ACK=1,和ack_seq=2001,seq=100

1,发送给服务器.至此客户端完成连接.

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

通过上面几个步骤,一个TCP连接就建立了.当然在建立过程中可能出现错误,不过TCP协议

可以保证自己去处理错误的.

说一说其中的一种错误.

听说过DOS吗?(可不是操作系统啊).今年春节的时候,美国的五大网站一起受到攻击.攻

击者用的就是DOS(拒绝式服务)方式.概括的说一下原理.

客户机先进行第一个步骤.服务器收到后,进行第二个步骤.按照正常的TCP连接,客户机

应该进行第三个步骤.

不过攻击者实际上并不进行第三个步骤.因为客户端在进行第一个步骤的时候,修改了自

己的IP地址,就是说将一个实际上不存在的IP填充在自己IP数据包的发送者的IP一栏.这

样因为服务器发的IP地址没有人接收,所以服务端会收不到第三个步骤的确认信号,这样

服务务端会在那边一直等待,直到超时.

这样当有大量的客户发出请求后,服务端会有大量等待,直到所有的资源被用光,而不能再

接收客户机的请求.

这样当正常的用户向服务器发出请求时,由于没有了资源而不能成功.于是就出现了春节

时所出现的情况.

----------------------------------------------------------------------------

 

网络编程(8)

 

8. 套接字选项

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

选项了.

8.1 getsockopt和setsockopt

int getsockopt(int sockfd,int level,int optname,void *optval,socklen_t *optl

en)

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

*optlen)

level指定控制套接字的层次.可以取三种值: 1)SOL_SOCKET:通用套接字选项. 2)IPPRO

TO_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 linge

r

SO_OOBINLINE 带外数据放入正常数据流 int

SO_RCVBUF 接收缓冲区大小 int

SO_SNDBUF 发送缓冲区大小 int

SO_RCVLOWAT 接收缓冲区下限 int

SO_SNDLOWAT 发送缓冲区下限 int

SO_RCVTIMEO 接收超时 struct timev

al

SO_SNDTIMEO 发送超时 struct timev

al

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 查看. 

--

 

网络编程(9)

 

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>;maxfd) maxfd=readfd;

while(1)

{

/* 将所有的文件描述符加入 */

FD_ZERO(&my_readfd);

for(i=0;i

FD_SET(readfd,*my_readfd);

/* 进程阻塞 */

select(maxfd+1,& my_readfd,NULL,NULL,NULL);

/* 有东西可以读了 */

for(i=0;i

if(FD_ISSET(readfd,&my_readfd))

{

/* 原来是我可以读了 */

we_read(readfd);

}

}

}

使用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 ;

#include ;

#include ;

#include ;

#include ;

#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调试 

阅读(867) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~