Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1438743
  • 博文数量: 704
  • 博客积分: 10140
  • 博客等级: 上将
  • 技术积分: 6230
  • 用 户 组: 普通用户
  • 注册时间: 2010-07-15 20:41
文章分类

全部博文(704)

文章存档

2013年(1)

2012年(16)

2011年(536)

2010年(151)

分类: C/C++

2011-01-06 22:45:26

网络编程socket之accept函数

摘要:对于服务器编程中最重要的一步等待并接受客户的连接,那么这一步在编程中如何完成,accept函数就是完成这一步的。它从内核中取出已经建立的客户连接,然后把这个已经建立的连接返回给用户程序,此时用户程序就可以与自己的客户进行点到点的通信了。

accept函数等待并接受客户请求:

#include
int accept(int sockfd, struct sockaddr* addr, socklen_t* len)
返回:非负描述字——成功, -1——失败

accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字。此时我们需要区分两种套接字,一种套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,一个套接字会从主动连接的套接字变身为一个监听套接字;而accept返回是一个连接套接字,它代表着一个网络已经存在的点点连接。自然要问的是:为什么要有两种套接字?原因很简单,如果使用一个描述字的话,那么它的功能太多,使得使用很不直观,同时在内核确实产生了一个这样的新的描述字。

参数sockfd
参数sockfd就是上面解释中的监听套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号正与这个套接字关联。当然客户不知道套接字这些细节,它只知道一个地址和一个端口号。
参数addr
这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。
参数len
如同大家所认为的,它也是结果的参数,用来接受上述addr的结构的大小的,它指明addr结构所占有的字节个数。同样的,它也可以被设置为NULL。

如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接字来完成与客户的通信。


网络编程socket之listen函数


摘要:listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。

listen函数在一般在调用bind之后-调用accept之前调用,它的函数原型是:

#include
int listen(int sockfd, int backlog)
返回:0──成功, -1──失败

参数sockfd
被listen函数作用的套接字,sockfd之前由socket函数返回。在被socket函数返回的套接字fd之时,它是一个主动连接的套接字,也就是此时系统假设用户会对这个套接字调用connect函数,期待它主动与其它进程连接,然后在服务器编程中,用户希望这个套接字可以接受外来的连接请求,也就是被动等待用户来连接。由于系统默认时认为一个套接字是主动连接的,所以需要通过某种方式来告诉系统,用户进程通过系统调用listen来完成这件事。
参数backlog
这个参数涉及到一些网络的细节。在进程正理一个一个连接请求的时候,可能还存在其它的连接请求。因为TCP连接是一个过程,所以可能存在一种半连接的状态,有时由于同时尝试连接的用户过多,使得服务器进程无法快速地完成连接请求。如果这个情况出现了,服务器进程希望内核如何处理呢?内核会在自己的进程空间里维护一个队列以跟踪这些完成的连接但服务器进程还没有接手处理或正在进行的连接,这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限。这个backlog告诉内核使用这个数值作为上限。
毫无疑问,服务器进程不能随便指定一个数值,内核有一个许可的范围。这个范围是实现相关的。很难有某种统一,一般这个值会小30以内。

当调用listen之后,服务器进程就可以调用accept来接受一个外来的请求。关于accept更的信息,请接着关注本系统文章。


网络编程socket之bind函数


摘要:在套接口中,一个套接字只是用户程序与内核交互信息的枢纽,它自身没有太多的信息,也没有网络协议地址和端口号等信息,在进行网络通信的时候,必须把一个套接字与一个地址相关联,这个过程就是地址绑定的过程。许多时候内核会我们自动绑定一个地址,然而有时用户可能需要自己来完成这个绑定的过程,以满足实际应用的需要,最典型的情况是一个服务器进程需要绑定一个众所周知的地址或端口以等待客户来连接。这个事由bind的函数完成。

从bind函数功能我们很容易推测出这个函数的需要的参数与相应的返回值,如果此时大家已经对socket接口有点熟悉了:

#include
int bind(int sockfd, struct sockaddr* addr, socklen_t addrlen)
返回:0──成功, -1──失败

参数sockfd
指定地址与哪个套接字绑定,这是一个由之前的socket函数调用返回的套接字。调用bind的函数之后,该套接字与一个相应的地址关联,发送到这个地址的数据可以通过这个套接字来读取与使用。
参数addr
指定地址。这是一个地址结构,并且是一个已经经过填写的有效的地址结构。调用bind之后这个地址与参数sockfd指定的套接字关联,从而实现上面所说的效果。
参数addrlen
正如大多数socket接口一样,内核不关心地址结构,当它复制或传递地址给驱动的时候,它依据这个值来确定需要复制多少数据。这已经成为socket接口中最常见的参数之一了。

bind函数并不是总是需要调用的,只有用户进程想与一个具体的地址或端口相关联的时候才需要调用这个函数。如果用户进程没有这个需要,那么程序可以依赖内核的自动的选址机制来完成自动地址选择,而不需要调用bind的函数,同时也避免不必要的复杂度。在一般情况下,对于服务器进程问题需要调用bind函数,对于客户进程则不需要调用bind函数。


网络编程socket之connect函数


摘要:网络编程socket api存在一批核心接口,而这一批核心接口就是几个看似简单的函数,尽管实际上这些函数没有一个是简单。connect函数就是这些核心接口的一个函数,它完成主动连接的过程。

connect函数的功能是完成一个有连接协议的连接过程,对于TCP来说就是那个三路握手过程,它的函数原型:

#include
int connect(int sockfd, const struct sockaddr* server_addr, socklen_t addrlen)
返回:0──成功, -1──失败。

为了理解connect函数,我们需要对connect函数的功能进行介绍。connect函数的功能可以用一句话来概括,就是完成面向连接的协议的连接过程,它是主要连接的。面向连接的协议,在建立连接的时候总会有一方先发送数据,那么谁调用了connect谁就是先发送数据的一方。如此理解connect三个参数是容易了,我必需指定数据发送的地址,同时也必需指定数据从哪里发送,这正好是connect的前两个参数,而第三个参数是为第二个参数服务的。

参数sockfd
指定数据发送的套接字,解决从哪里发送的问题。内核需要维护大量IO通道,所以用户必需通过这个参数告诉内核从哪个IO通道,此处就是从哪个socket接口中发送数据。sockfd是先前socket返回的值。
参数server_addr
指定数据发送的目的地,也就是服务器端的地址。这里服务器是针对connect说的,因为connect是主动连接的一方调用的,所以相应的要存在一个被连接的一方,被动连接的一方需要调用listen以接受connect的连接请求,如此被动连接的一方就是服务器了。
参数addrlen
指定server_addr结构体的长度。我们知道系统中存在大量的地址结构,但socket接口只是通过一个统一的结构来指定参数类型,所以需要指定一个长度,以使内核在进行参数复制的时候有个有个界限。

与所有的socket网络接口一样,connect总会在某个时候可能失败,此时它会返回-1,相应的errno会被设置,用户可能通过这个值确定是哪个错误。常见的错误有对方主机不可达或者超时错误,也可以是对方主机没有相应的进程在对应端口等待。

网络编程socket之socket函数

摘要:socket函数是任何套接口网络编程中第一个使用的函数,它向用户提供一个套接字,即套接口描述文件字,它是一个整数,如同文件描述符一样,是内核标识一个IO结构的索引。通过socket函数,我们指定一个套接口的协议相关的属性,为进行使用socket api做好准备。

如同所有的介绍API的文档一样,我们先给出socket函数的原型:

#include
int socket(int family, int type, int protocol)
返回:非负描述字──成功, -1──出错

参数family
这个参数指定一个协议簇,也往往被称为协议域。系统存在许多可以的协议簇,常见有AF_INET──指定为IPv4协议,AF_INET6──指定为IPv6,AF_LOCAL──指定为UNIX 协议域等等。它值都是系统预先定义的宏,系统支持哪些协议我们才可以使用,否则会调用失败。协议簇是网络层的协议。
参数type
这个参数指定一个套接口的类型,套接口可能的类型有:SOCK_STREAM、SOCK_DGRAM、SOCK_SEQPACKET、SOCK_RAW等等,它们分别表明字节流、数据报、有序分组、原始套接口。这实际上是指定内核为我们提供的服务抽象,比如我们要一个字节流。需要注意的,并不是每一种协议簇都支持这里的所有的类型,所以类型与协议簇要匹配。
参数protocol
指定相应的传输协议,也就是诸如TCP或UDP协议等等,系统针对每一个协议簇与类型提供了一个默认的协议,我们通过把protocol设置为0来使用这个默认的值。注意这里的协议与上面的协议簇是两个不同的概念,前者是指网络层的协议,由于它对于到传输层会出现许多协议,比如IPv4可以用来实现TCP或UDP等等传输层协议,所以称为协议簇。相应的传输层的协议就简单地称为协议。常见的协议有TCP、UDP、SCTP,要指定它们分别使用宏IPPROTO_TCP、IPPROTO_UPD、IPPROTO_SCTP来指定。
返回值
socket函数返回一个套接字,即套接口描述字。如果出现错误,它返回-1,并设置errno为相应的值,用户应该检测以判断出现什么错误。

下面给出几个使用socket函数的示例:

#include
//使用IPv4作为协议簇,使用字节流类型,
//使用系统针对IPv4与字节流的默认的协议,一般为TCP
int sockfd=socket(AF_INET, SOCK_STRAM, 0);

//使用STCP作为协议
int sockfd=socket(AF_INET, SOCK_STRAM, IPPROTO_SCTP);

//使用数据报
int sockfd=socket(AF_INET, SOCK_DGRAM, 0);

网络编程之socket套接口地址结构


摘要:socket api是网络编程的经典选择,可能在许多时候也是不二选择,本文介绍socket套接口中存在的一个知识点:套接口地址结构。socket编程中常存在一个问题,就是填写一个地址结构,然后绑定相应地址,所以对地址结构的理解是基本的要求。

在我第一次写网络程序的时候,或者说我第一次使用socket的时候,深深地感觉到socket中的地址填写有点难写或者有点难受,这个难受在于第一次接触socket本身的生疏,也在于socket地址看上去有点复杂和乱。出于这个原因,Stevens在他们的《UNIX网络编程》中拿一章来说明这个地址结构。

socket接口中地址结构有很多,这是一个基本的事实,但对于多数程序来说可能只会用到其中的少数。这些地址结构存在两类完全不同目的的结构,一类是实际中使用的结构,一类却是只是为参数传递的结构。前者与具体协议有关,不同的地址结构之间的差异很大;而后者就只有两个结构,称为通过结构。

地址结构的内容

我在这里不准备具体介绍哪一个地址结构,而是就地址结构的总体上来说明一下。在地址结构上倾向于为它自身指定一个长度,也就是地址结构往往会有一个长度成员,它指出当前正在使用结构体占有多少字节,但是由于历史原因并不能保证每个实现中都存在这个成员,所以看上去有的这个结构成员可能没有用处──在用户层上这个成员总是很少使用。并且socket api中问题假设用户没有指定这个成员,从而要求把这个长度的值显式地通过参数传递给这些api,如果connect、bind等等。这一点让人难受,有而不用

在一些地址结构中存在一个未用的扩展区,也就是这个地方现在没有使用,但是在以后的某个时候可能会使用。这样就给人一种感觉,如果有一天这个地方使用了,那么我现在写的程序怎么办,总叫人不放心。可是socket已经产生了几十年,这个预留的东西从来没有使用过,所以历史告诉我们这些东西将来也不会使用。但是出于安全等等各种因素我们需要把这块内存处理为0。

在地址结构存在一个这样的成员它指定相应的地址簇或者说协议簇,它会非常有用,内核会根据这个值来对传入的内容作不同的处理。我们可能已经注意到了,诸如connect、bind这些接口并没有与具体地址结构相关的,所以但是内核必须在某个时候来区分这些地址结构,无论内核在哪里区分它们,区分他们总要一个依据,这个成员就是依据。

地址结构的其它成员就与具体的协议有关了,它们都协议相关的地址结构,各个协议之间可能存在巨大差异,所以我们在这里就不说了。

地址结构的使用

地址结构各有不同,所以用户在使用地址结构的时候必须要知道自己需要使用什么,内核不会为我们做决定,做决定是在用户层上进行的。这就是说,用户需要在使用不同的协议的时候选择一个合适的地址结构并正确的填写其中的成员,接着把填写好的结构交内核,由内核作进一步处理。

由于历史原因,我们可能没有办法知道或者确切了解一个结构成员,所以我们不能一个成员一个成员地来处理,在这里我们使用一个原则:只设置我们感兴趣的成员,把不感兴趣的成员都设置为0。如此就有了一个基本的思路:把整个结构置0,然后把设置我们感兴趣的成员为合适的值。

此外地址结构中大多数成员都使用是网络字节序列,所以用户在设置成员值的时候可能需要把机器字节序列调整为网络字节序列,socket给我们两个工具了(htonl一簇函数),但在网络应用中,可能还需要更强大的工具,此时需要我们手工调整字节序列。

socket套接口地址结构本身可能很复杂,再加上历史原因又加上一些人为的复杂性,所以学习与使用的时候需要小心一些。不过相对socket其它复杂的地方,地址结构可能还是相对简单的。


网络编程socket之close与shutdown函数


摘要:对于网络TCP面向连接的程序,它需要在某个时候终止已经存在的连接。用户可以主动终止一个连接,这很重要,尤其对于服务器进程而言,因为一个进程可以同时打开的连接是有限的,如果不在某个时候主动终止已有的连接,那么对于服务器进程来说,它总会在某个时候因为无法打开新连接而失败。

对于UNIX系统而言,无论是一般的文件描述符,还是网络中使用的套接字都是描述字的范围,所以它们都可以用close函数来完成关闭的任务,然后对于网络套接字这一个特殊的描述字,我们却可以使用更加丰富的shutdown函数完成有选择的关闭。下面我们先来看看这个两个函数:

#include
int close(int fd)
返回:0——成功, -1——失败

#include
int shutdown(int sockfd, int howto)
返回:0——成功, -1——失败

让我们来回忆一下,一个文件描述符关联着一个实际的文件——不管这个文件是什么,普通文件或网络套接口等等,但是多个打字可以同时与一个文件关联,并且内核维护一个文件引用计数。正常情况下,close函数不武断地释放一个描述字关联的文件,除了这个引用计数为0的时候,并且无论如何,当对一个描述字调用了close函数,用户无法再次使用这个描述字。这是close相对shutdown的两点差别,相应地shutdown是针对socket套接口定制的函数,所以它会做的更好。

shutdown函数不是参考引用计数,它会直接关闭相应的socket套接口,无论引用计数是多少。我们还知道,socket套接口是全双工的,也就是用户可以读,也可以写。存在一个这样的情况,此时用户已经把所有要写的数据都写完了,他想告诉对等端这一点;或者用户把所有要读的数据都读完成了,同样要告诉对等端。此时就是关闭读这一半或写这一半,使用shutdown可以完成这一个。系统定义了3个宏,这3个宏分别用作shutdown的后一个参数:

  • SHUT_RD:关闭读这一半,此时用户不能再从这个套接字读数据,这个套接口接收到的数据都会被丢弃,对等方不知道这个过程。
  • SHUT_WR:相应地关闭写这一半,此时用户不能再向套接字中写数据,内核会把缓存中的数据发送出去,接着不会再发送数据,对等端将会知道这一点。当对等端试图去读的时候,可能会发生错误。
  • SHUT_RDWR:关闭读与写两半,此时用户不能从套接字中读或写。它相当于再次调用shutdown函数,并且一次指定SHUT_RD,一次指定SHUT_WR。

刚才我写完了,这个文章中我们使用close函数,试问一下:我们可以使用shutdown函数代替吗?简单的思考之后,我们知道不可以使用shutdown函数代替,因为我们在子进程中只是想解除sockfd与那个监听套接口的关联,并不想释放这个套接口,原因是在父进程还要使用它;相应在父进程我也只是想解除cfd与其套接口的关联,我们在子进程还需要使用cfd。从这个例子中可以看到,close与shutdown有各自的用处,并不能相互代替,就算在socket套接字这一特定情况下也如此。



网络编程socket之服务器编程的挑战


摘要:网络服务器编程是一个巨大的挑战,这个挑战来自服务器需要完成的任务太多,这个挑战来自可能存在的恶意或无意破坏,这个挑战来自大量的与内核的交互,同时这个挑战也来自网络本身的极端不稳定性。在面对这些挑战时,服务器却被要求为快速、稳定、可用的结合物!本文关注这些挑战,并讨论如何避免其中遇到的一些问题。

让我们逐一来讨论上面说到的问题,首先说说服务器要完成的任务太多。现在的服务器大多非常繁忙,比如每秒都可以需要处理上千上万个客户请求,这对于稍等大一点网站或者游戏服务器来说都是经常的事。此时我们有两个思路来解决这个问题,一个思路是提高硬件的速度,另一个思路就是提高软件的性能,通常情况是两路齐走。本文不说硬件,所以只是软件这条思路如何走。

为了提高软件性能当然是算法,此时我必须使用并行的算法来处理,并行是当前提高恨不能的必要方法,也是基本方法。在一台主机上的并行是通过软件来实现的,此时有进程模式和线程模式,由于现代的操作系统已经很强大了,所以我们还可以依赖操作系统提供的IO复用、异步IO等等功能实现隐匿的并行。当然后者简单易于实现,但性能的提升空间却没有手工并行再加上此类技术大,所以人们都会手工实现线程或进程,然后在线程或进程内部再使用IO复用、异步IO等技术,所谓为性能无所不用其极。

服务器编程的另一个挑战是需要识别有意无意的破坏,或把有意无意的破坏限制在最小的范围内,单一客户的请求应该是独立与其他用户的请求的,此时一个用户的失败不至于影响其他用户,从而把这种失败破坏限制在有限的范围内。服务器最常见的破坏就是拒绝服务器攻击,如果没有此种特性的服务器,面对拒绝服务攻击只能听之任之了。有时服务器可能需要维护一个统计信息,这样可以比较容易识别出有意破坏,但这一思路可以提高服务器的可用性的同时却降低了服务器的速度。对,服务器的三个特性有时是相互矛盾的,我们提高了其中一个,相应的另一个却在下降,此时需要一种权衡,或更复杂的决择机制。分层或分级是此时我没用的策略,在不同的级别上采用不同的决择。并行在此时仍然是基本的思路与措施。



socket网络编程:IO复用_select函数


摘要:在UNIX系统编程过程中,一个进程往往需要等待多个描述字发生某一事件,如可读、可写或异常等等。进程不能永远地等待其中任何单独一个描述字,它需要同时等待所有描述字,此时就是IO复用技术,系统调用select就是实现这一目标的方式之一。本文详细介绍select函数。

select广泛应用于各种场合,因为select对于任何描述字都有作用,它被应用网络程序,也被应用于终端程序,也被应用其它场合。下面我们先来看看它的函数原型:

#include
#include
int select(int max_fd_p_1, fd_set *readset, fd_set* writeset, fd_set* exceptset,struct timeval* timeout);
返回:就绪描述字的个数,0——超时,-1——出错

select函数有5个参数,我们会介绍每一个参数。

参数max_fd_p_1
内核需要一个数字来指定最大的描述字,因为内核使用这个参数来遍历一组描述字。又因为描述字是从0开始计数的,所以max_fd_p_1实现上是最大描述字的加上1的值。
参数readset
select可以测试一组描述字是否可读,用户需要告诉内核所有关心的描述字,readset就是用来指定关心的描述字集合。
参数writeset
如同readset指定可读描述字集合一样,writeset用来指定关心的可写描述字集合。
参数exceptset
跟readset、writeset一样,exceptset用来指定所关心的异常描述字集合,也就是当集合中一个描述字出现异常时会得到内核的一个通知。
参数timeout
用来指定超时的时长,这是一个结构体:
struct timeval
{
     long tv_sec;//秒数
     long tv_usec;//微秒数
}
这个结体定义在头文件sys/time.h中。

上面我们简单地针对select自身进行了说明。我们注意到,select的参数有4个是指针,如果这些指针取值是空的话会如何?对于指定描述字集合的参数,如果取空的话,它们的意义很直接,就是没有相应的关心的描述字集合。比如readset==NULL,则说明用户不关心任何一个描述字是否可读,对于writeset、exceptset也完全一样。但是对于timeout==NULL的情况就是很直接的,它表示永远等待下去,此时我们不关心需要等待多长时间,我们只要求至少有一个描述字满足用户所关心的。

对于timeout里面两个成员都取值0的时候,相应意义很直接,就是等待0秒0微秒,也就是说不等待任何时间。此时就相当于简单的轮询。

上面我们介绍了最后一个参数的,那么先前的三个参数我需要注意一下,就是它们的类型是fd_set,fd_set是什么样的类型呢?我并不知道,但有一点,就是POSIX标准为我们提供了四个宏,这四个宏可以完成我们需要对fd_set的操作,而fd_set的实际类型留给系统去定义。这四个宏分别是:

void FD_ZERO(fd_set* set);//把set设置为0
void FD_SET(int fd, fd_set* set);//从把fd添加到set
void FD_CLR(int fd, fd_set* set);//从set中删除fd
int FD_ISSET(int fd, fd_set* set);//判断fd是否在set中被设置
使用这四个宏,我们就可以完成fd_set相关的所有操作了。

select是一相十分复杂的函数,它的返回值如上所说,-1为错误,0为超时,正数为就绪的打字个数。但是事实上打字的个数往往很小,系统会一个限制,一般为1024;这是因为fd_set这个类型的限制,同时select也不应该处理过多的打字,否则话会有性能问题。

select也会设置errno值,最常见的值可能就是EINT,表示一个信号中断了select调用,作为应用程序应该在select返回错误的时候查看errno。

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