第四章 网络编程socket

转自http://cpp.ezbty.org/content/doc_list/socket%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B

本章阐述编写一个完整的TCP客户和服务器程序所需要的基本套接口函数

4.2 socket函数

为了执行网络I/O,一个进程必须做的第一件事情就是调用socket函数,指定期望的通信协议类型(使用IPv4的TCP、使用IPv6的UDP、Unix域字节流协议等)。

#include <sys/socket.h>

int socket(int family, int type, int protocol);

return:若成功则为非负描述符,若出错则为-1;

代码中的family指明协议族;

Family                           说明

AF_INET                        IPv4协议

AF_INET6                      IPv6协议

AF_LOCAL                      Unix域协议(第15章)

AF_ROUTE                     路由套接字(第18章)

AF_KEY                         密钥套接字(第19章)

图4.2 socket函数的family常值


套接口的类型type是图4.3中所示的某个常值;

Type                            说明

SOCK_STREAM               字节流套接字

SOCK_DGRAM                 数据报套接字

SOCK_SEQPACKET           有序分组套接字

SOCK_RAW                    原始套接字

图4.3 socket函数的type常值


函数socket的参数protocol设置为0;

4.3 connect函数 80

TCP客户用connect函数来建立一个与TCP服务器的连接

#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *servaddr,socklen_t addrlen);

return:若成功则为0,若出错则为-1;

sockfd是由socket函数返回的套接口描述字,第二第三个套接口分别是指向套接口地址结构的指针和该结构的大小。如,3.3节所示。

4.4 bind函数

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

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

#include<sys/socket.h>
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函数。


4.5 listen函数

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

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

#include<sys/socket.h>
int listen ( int sockfd, int backlog )
返回: 0──成功, - 1──失败
参数sockfd
被listen函数作用的套接字,sockfd之前由socket函数返回。在被socket函数返回的套接字fd之时,它是一个主动连接的套接字,也就是此时系统假设用户会对这个套接字调用connect函数,期待它主动与其它进程连接,然后在服务器编程中,用户希望这个套接字可以接受外来的连接请求,也就是被动等待用户来连接。由于系统默认时认为一个套接字是主动连接的,所以需要通过某种方式来告诉系统,用户进程通过系统调用listen来完成这件事。

参数backlog
这个参数涉及到一些网络的细节。在进程正理一个一个连接请求的时候,可能还存在其它的连接请求。因为TCP连接是一个过程,所以可能存在一种半连接的状态,有时由于同时尝试连接的用户过多,使得服务器进程无法快速地完成连接请求。如果这个情况出现了,服务器进程希望内核如何处理呢?内核会在自己的进程空间里维护一个队列以跟踪这些完成的连接但服务器进程还没有接手处理或正在进行的连接,这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限。这个backlog告诉内核使用这个数值作为上限。
毫无疑问,服务器进程不能随便指定一个数值,内核有一个许可的范围。这个范围是实现相关的。很难有某种统一,一般这个值会小30以内。

4.6 accept函数

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

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

#include<sys/socket.h>
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返回的套接字来完成与客户的通信。

4.7 fork与exec函数

摘要:网络编程本身并不复杂或多么困难,复杂或困难的是在不稳定的网络中做到稳定的服务器与通信,此时并行技术是实现的稳定的服务器的技术之一,当然也不是服务器端使用的技术。fork是POSIX系统中产生新进程的唯一方法,可以实现进程并行编程模式。

在介绍服务器编程的fork模式之前,我们首先来说说fork自身的问题,对于深知fork的读者可以跳过本段内容。fork通常作为一个系统调用来存在,或者作为一个系统调用的简单封装,它对于内核来说只做一件事:把当前进程的内存镜像复制一份,以在系统中产生一个新进程,并在两个进程中各返回一次,新产生的进程与原本的进程完全相同,它们共享着文件描述符,如此在两个进程中可以对同一文件进行操作,套接字也如此。

服务器编程的fork并行模式的基本的思想是:一个进程监听一相应的端口,当取得一个客户连接的时候,它使用fork产生一个新进程,由于新进程是原进程的复本,所以它可以与客户完成通信,而原来的父进程仍然继续监听那个端口,下面是一个典型的代码片断:

#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>

int main ( )
{
        //如同串行的程序,这之前完成许多初始工作
        //下面代码假设sockfd是一个监听套接字

        while ( 1 )
        {
                //如果想知道客户地址,后面两个参数可以指定相应的结构
                int cfd =accpet (sockfd, NULL, NULL ) ;
                int pid =fork ( ) ;
                if ( 0 >pid )
                {
                        //创建进程失败,我们直接退出服务器
                        //这只是一个示例程序
                        return - 1 ;
                }
                else if (0 <pid )
                {
                        close (cfd ) ;
                        continue ;
                }

                //此处代码我意在让新进程运行,此时pid==0是明显的
                close (sockfd ) ;
               
                //新进程使用cfd与客户进程进行通信
                //在通信结束之后,新进程也许就直接退出了
        }
}

上述的代码片断只是一个示例性的,以简单明了为基础,但它却直接而不明白反应了fork并行模式的基本思想。在实际中的程序还可以需要对信号进行处理,尤其当新进程比父进程早退出的时候,父进程会收到一个SIGCHILD信号,如果父进程不wait或waitpid的话,它将变成一个僵死进程,显然这是不希望的。


4.8 并发服务器

网络编程socket之close与shutdown函数

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

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

#include<unistd.h>
int close ( int fd )
返回: 0——成功, - 1——失败

#include<sys/socket.h>
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。

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


4.9 close 函数

4.10 getsockname和getpeername函数

4.11 小结

4.12 习题


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值