zoukankan      html  css  js  c++  java
  • Socket与系统调用深度分析

    1 Socket与系统调用

    1)Socket基本组成结构

      Socket是相同主机进程之间或者不同主机进程之间进行通信的主流手段之一,两台计算机之间的网络通信可以通过在各自的系统中创建一个Socket,进而利用它来实现相互之间的通信。

      那Socket究竟是什么呢?一个基本的Socket就是由本机IP,本机进程端口,目的IP,和目的进程端口,以及输入输出缓冲组成的一个数据结构。其中前四个属性分别用来标识本机信息和目的计算机的信息,输入输出缓冲用来暂存保存通信的数据。

    2)Socket通信连接步骤

      Socket连接建立之前主要进行两项工作,第一项是连接建立前的两个Socket的初始化工作,第二个是两台计算机通信时的“三路握手”。

    (1)两个Socket的初始化

      服务端

      首先,用C语言通过系统调用socket()函数来创建套接口。通过以下程序段便可以建立一个用TCP的Socket:int listensockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)

      其次,通过系统调用int bind ()函数来初始化Socket中的本地IP地址和本地端口号。通过以下语句,便可以初始化服务器端的Socket:bind( listensockfd,(struct sockaddr*) &saServer, sizeof(saServer));

      最后,我们通过系统调用listen()函数来将已经绑定了本机IP地址和程序端口号的Socket的状态由主动(positive)转换为被动(passive)。并调用accept()函数来获取已经准备好的套接字准备收取数据。通过以下程序段,便可以启动监听Socket,并返回Q1队列中和客户端“三路握手”完成的套接字。

      listen( listensockfd, 5 );  int acceptSocket = accept( listenSocketfd, NULL,NULL );

      客户端    

      通过Socket系统调用建立Socket,然后初始化客户端的IP地址和端口号,并通过connect()函数将初始化好的IP地址和端口号的数据结构绑定在新建的Socket上,与服务器端不同的是这个数据结构是为了初始化客户端的IP地址和进程端口。

    (2)两台计算机通信时的“三路握手”    

      第一次握手:客户端调用connect()函数将目的地(服务器端)的IP地址和进程端口初始化的同时,给内核运输层发出指令,使其将封装好(其中包含完整的四元组)的数据包(同步包,下面简称SYN包),通过更底层的协议层向目的地(服务器端)传送,以发出请求。    

      第二次握手:当目的地(服务器端)收到客户端发送的SYN包时,如果请求可以通过,服务器端也通过运输层封装好一个包含通过请求的SYN+ACK包,否则封装一个拒绝请求的SYN+NACK包发送给客户端,于此同时,服务器内核自动创建一个Socket,并将已将创建好的监听套接口的本地IP地址和进程端口拷贝到新创建的Socket中的本地IP地址和进程端口中,将第一次握手过程中的SYN包中的客户端的IP地址和端口号拷贝在新创建的Socket中的目的地IP地址和端口号中,然后将新创建的Socket放入监听Socket中的Q0队列中。

      第三次握手:当客户端收到服务器端回应的SYN+ACK包时,客户端需要再返回给服务器一个SYN包表示已经收到SYN+ACK包,与此同时,服务器将刚才放入Q0队列的Socket放入监听套接口的Q1队列中(用于放置内核为服务器和不同客户端通信创建的已经完成“三路握手”Socket),而客户端通过系统调用accept()正是获取的Q1队列中的套接字。

    3)通过socket进行通信  
      服务器端通过系统调用recv()进行数据的获取,客户端可以通过调用send()进行数据的发送。

    4)系统调用函数

    (1)socket()函数

      socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

    正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

    • protofamily:即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
    • type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
    • protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

    (2)bind()函数

    int bind(
    SOCKET s,
    const struct sockaddr FAR *name,
    int namelen
    );

    当创建了一个Socket以后,套接字数据结构中有一个默认的IP地址和默认的端口号。一个服务程序必须调用bind函数来给其绑定一个IP地址和 一个特定的端口号。客户程序一般不必调用bind函数来为其Socket绑定IP地址和断口号。该函数的第一个参数指定待绑定的Socket描述符;第二 个参数指定一个sockaddr结构,该结构是这样定义的:
    struct sockaddr {
    u_short sa_family;
    char sa_data[14];
    };

    其中sin_family置AF_INET;sin_port指明端口号;sin_addr结构体中只有一个唯一的字段s_addr,表示IP地 址,该字段是一个整数,一般用函数inet_addr()把字符串形式的IP地址转换成unsigned long型的整数值后再置给s_addr。有的服务器是多宿主机,至少有两个网卡,那么运行在这样的服务器上的服务程序在为其Socket绑定IP地址时 可以把htonl(INADDR_ANY)置给s_addr,这样做的好处是不论哪个网段上的客户程序都能与该服务程序通信;如果只给运行在多宿主机上的 服务程序的Socket绑定一个固定的IP地址,那么就只有与该IP地址处于同一个网段上的客户程序才能与该服务程序通信。我们用0来填充 sin_zero数组,目的是让sockaddr_in结构的大小与sockaddr结构的大小一致。下面是一个bind函数调用的例子:
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(8888);
    saddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bind(ListenSocket,(struct sockaddr *)&saddr,sizeof(saddr));

    (3)listen函数

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

    intlisten(int sockfd, int backlog)

    参数sockfd

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

    参数backlog

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

    毫无疑问,服务器进程不能随便指定一个数值,内核有一个许可的范围。这个范围是实现相关的。很难有某种统一,一般这个值会小30以内。

    (4)accept函数

    accept()用来接受参数s的socket连接,它的函数原型是:

    intaccept(int s,struct sockaddr * addr,int * addrlen)

    服务程序调用accept函数从处于监听状态的流套接字s的客户连接请求队列中取出排在最前的一个客户请求,并且创建一个新的套接字来与客户套接字创建连接通道,如果连接成功,就返回新创建的套接字的描述符,以后与客户套接字交换数据的是新创建的套接字;如果失败就返回 INVALID_SOCKET。该函数的第一个参数指定处于监听状态的流套接字;操作系统利用第二个参数来返回新创建的套接字的地址结构;操作系统利用第三个参数来返回新创建的套接字的地址结构的长度。

    2 跟踪分析Linux/Socket系统调用

      在分析系统调用时,我们常用gdb starce来进行分析。strace常用来跟踪进程执行时的系统调用和所接收的信号,调试应用程序的时候经常使用。 在Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通 过系统调用访问硬件设备。strace可以跟踪到一个进程产生的系统调用,包括参数,返回值,执行消耗的时间。

    strace用法:

    -c 统计每一系统调用的所执行的时间,次数和出错的次数等. 
    -d 输出strace关于标准错误的调试信息. 
    -f 跟踪由fork调用所产生的子进程. 
    -ff 如果提供-o filename,则所有进程的跟踪结果输出到相应的filename.pid中,pid是各进程的进程号. 
    -F 尝试跟踪vfork调用.在-f时,vfork不被跟踪. 
    -h 输出简要的帮助信息. 
    -i 输出系统调用的入口指针. 
    -q 禁止输出关于脱离的消息. 
    -r 打印出相对时间关于,,每一个系统调用. 
    -t 在输出中的每一行前加上时间信息. 
    -tt 在输出中的每一行前加上时间信息,微秒级. 
    -ttt 微秒级输出,以秒了表示时间. 
    -T 显示每一调用所耗的时间. 
    -v 输出所有的系统调用.一些调用关于环境变量,状态,输入输出等调用由于使用频繁,默认不输出. 
    -V 输出strace的版本信息. 
    -x 以十六进制形式输出非标准字符串 
    -xx 所有字符串以十六进制形式输出. 
    -a column 
    设置返回值的输出位置.默认 为40. 
    -e expr 
    指定一个表达式,用来控制如何跟踪.格式如下: 
    [qualifier=][!]value1[,value2]... 
    qualifier只能是 trace,abbrev,verbose,raw,signal,read,write其中之一.value是用来限定的符号或数字.默认的 qualifier是 trace.感叹号是否定符号.例如: 
    -eopen等价于 -e trace=open,表示只跟踪open调用.而-etrace!=open表示跟踪除了open以外的其他调用.有两个特殊的符号 all 和 none. 
    注意有些shell使用!来执行历史记录里的命令,所以要使用\. 
    -e trace=set 
    只跟踪指定的系统 调用.例如:-e trace=open,close,rean,write表示只跟踪这四个系统调用.默认的为set=all. 
    -e trace=file 
    只跟踪有关文件操作的系统调用. 
    -e trace=process 
    只跟踪有关进程控制的系统调用. 
    -e trace=network 
    跟踪与网络有关的所有系统调用. 
    -e strace=signal 
    跟踪所有与系统信号有关的 系统调用 
    -e trace=ipc 
    跟踪所有与进程通讯有关的系统调用 
    -e abbrev=set 
    设定 strace输出的系统调用的结果集.-v 等与 abbrev=none.默认为abbrev=all. 
    -e raw=set 
    将指 定的系统调用的参数以十六进制显示. 
    -e signal=set 
    指定跟踪的系统信号.默认为all.如 signal=!SIGIO(或者signal=!io),表示不跟踪SIGIO信号. 
    -e read=set 
    输出从指定文件中读出 的数据.例如: 
    -e read=3,5 
    -e write=set 
    输出写入到指定文件中的数据. 
    -o filename 
    将strace的输出写入文件filename 
    -p pid 
    跟踪指定的进程pid. 
    -s strsize 
    指定输出的字符串的最大长度.默认为32.文件名一直全部输出. 
    -u username 
    以username 的UID和GID执行被跟踪的命令

      gdb(GNU symbolic debugger)是一个由GNU开源组织发布的、UNIX/LINUX操作系统下的、基于命令行的、功能强大的程序调试工具。

     一般来说,GDB主要帮助你完成下面四个方面的功能:

    1、启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。

    2、可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)

    3、当程序被停住时,可以检查此时你的程序中所发生的事。

    4、你可以改变你的程序,将一个BUG产生的影响修正从而测试其他BUG。

    start           #开始调试,停在第一行代码处,(gdb)start
    l               #list的缩写查看源代码,(gdb)l
    b <lines>       #b: Breakpoint的简写,设置断点。(gdb) b 8 
    b <func>        #b: Breakpoint的简写,设置断点。(gdb) b main
    i breakpoints   #i:info 的简写。(gdb)i breakpoints
    d [bpNO]        #d: Delete breakpoint的简写,删除指定编号的某个断点,或删除所有断点。断点编号从1开始递增。(gdb)d 1
    ​
    s               #s: step执行一行源程序代码,如果此行代码中有函数调用,则进入该函数;(gdb) s
    n               #n: next执行一行源程序代码,此行代码中的函数调用也一并执行。(gdb) n
    ​
    r               #Run的简写,运行被调试的程序。如果此前没有下过断点,则执行完整个程序;如果有断点,则程序暂停在第一个可用断点处。(gdb) r
    c               #Continue的简写,继续执行被调试程序,直至下一个断点或程序结束。(gdb) c
    finish          #函数结束
    ​
    p [var]             #Print的简写,显示指定变量(临时变量或全局变量 例如 int a)的值。(gdb) p a
    display [var]       #display,设置想要跟踪的变量(例如 int a)。(gdb) display a
    undisplay [varnum]  #undisplay取消对变量的跟踪,被跟踪变量用整型数标识。(gdb) undisplay 1
    set args            #可指定运行时参数。(gdb)set args 10 20
    show args           #查看运行时参数。
    q                   #Quit的简写,退出GDB调试环境。(gdb) q 
    help [cmd]          #GDB帮助命令,提供对GDB名种命令的解释说明。如果指定了“命令名称”参数,则显示该命令的详细说明;如果没有指定参数,则分类显示所有GDB命令,供用户进一步浏览和查询。(gdb)help
    回车                #重复前面的命令,(gdb)回车

    然后是内核跟踪:

    打开gdb调试,将与socket相关的函数们都打上断点

    然后按c让程序继续执行,但运行完了replyhi程序和hello程序并成功进行网络通信后左边的gdb却并未捕获到断点,说明没有执行sys_bind和sys_listen系统调用。

    给sys_socketall打上断点

     打上断点后并继续执行,等待服务端连接

     在Qemu中输入hello,输入c,继续按回车继续,可以成功捕获:

  • 相关阅读:
    MvcApplication 中方法的那点事
    Html 中阻止事件冒泡的三种方法比较
    WPF中 ItemsSource 和DataContext不同点
    解决:Visual Studio 启动就报错退出
    webapi是如何绑定参数的(How WebAPI does Parameter Binding)
    %cd% 和%~dp0%的区别及cd跨盘符切换路径问题
    win10中matlabR2015b安装libsvm
    MATLAB2015b链接MinGW编译器
    网易内推编程题:异或运算求混合颜料的最小种类
    小易喜欢的单词
  • 原文地址:https://www.cnblogs.com/sovegetabable/p/12067582.html
Copyright © 2011-2022 走看看