socket通信讲解


TCP/IP通信示意图


Connect后服务器使用accept创建新的socket(ns),

原有socket s

请求数据、回应数据流程

Socket ns write——socket s read——socket swrite(回应数据)——Socketns read




socket结构体对象定义

    

在使用socket编程之前,需要调用socket函数创建一个socket对象,该函数返回该socket对象的描述符。

 

函数原型:int socket(int domain, int type, int protocol);

    

socket  结构体的定义如下:   

struct socket   

{   

    socket_state              state;   

    unsigned long             flags;   

    const struct proto_ops    *ops;   

    struct fasync_struct      *fasync_list;   

    struct file               *file;   

    struct sock               *sk;   

    wait_queue_head_t         wait;   

    short                     type;   

};

    

其中,struct sock包含有一个sock_common 结构体,而sock_common结构体又包含有struct inet_sock 结构体,而struct inet_sock 结构体的部分定义如下:

 

struct inet_sock   

{   

    struct sock     sk;   

#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)   

    struct ipv6_pinfo   *pinet6;   

#endif   

    __u32           daddr;          //IPv4的目的地址。   

    __u32           rcv_saddr;      //IPv4的本地接收地址。   

    __u16           dport;          //目的端口。   

    __u16           num;            //本地端口(主机字节序)。  

    

    …………      

}

     

socket结构体不仅仅记录了本地的IP和端口号,还记录了目的IP和端口。

     

connect函数究竟做了些什么操作

     

在TCP客户端,首先调用一个socket()函数,得到一个socket描述符socketfd,然后通过connect函数对服务器进行连接,连接成功后,就可以利用这个socketfd描述符使用send/recv函数收发数据了。

    

关于connect函数和send函数的原型如下:

 

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

 

int send( int sockfd, const void *msg,int len,int flags);

    

那么,现在的困惑是,为什么send函数仅仅传入sockfd就可以知道服务器的ip和端口号?

    

sockfd 描述符所描述的socket对象不仅包含了本地IP和端口,同时也包含了服务器的IP和端口,这样,才能使得send函数只需要传入sockfd 即可知道该把数据发向什么地方。而代码中,目的IP和端口只是在connect函数中出现过,因此,肯定是connect函数在成功建立连接后,将目的IP和端口写入了sockfd 描述符所描述的socket对象中。

    

accept函数产生的socket有没有占用新的端口?

    

accept函数,原型如下:

 

/* 参数:sockfd 监听套接字,即服务器端创建的用于listen的socket描述符。  

 * 参数:addr  这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址  

 * 参数:len 描述 addr 的长度  

 */ 

int accept(int sockfd, struct sockaddr* addr, socklen_t* len)

    

accept函数主要用于服务器端,一般位于listen函数之后,默认会阻塞进程,直到有一个客户请求连接,建立好连接后,它返回的一个新的套接字 socketfd_new ,此后,服务器端即可使用这个新的套接字socketfd_new与该客户端进行通信,而sockfd 则继续用于监听其他客户端的连接请求。

至此,我的困惑产生了,这个新的套接字 socketfd_new 与监听套接字sockfd 是什么关系?它所代表的socket对象包含了哪些信息?socketfd_new是否占用了新的端口与客户端通信?

    

先简单分析一番,由于网站的服务器也是一种TCP服务器,使用的是80端口,并不会因客户端的连接而产生新的端口给客户端服务,该客户端依然是向服务器端的80端口发送数据,其他客户端依然向80端口申请连接。因此,可以判断,socketfd_new 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd_new一样的端口号。

    

那这么说,难道一个端口可以被两个socket对象绑定?当客户端发送数据过来的时候,究竟是与哪一个socket对象通信呢?

我是这么理解的(欢迎拍砖)。

    

首先,一个端口肯定只能绑定一个socket。我认为,服务器端的端口在bind的时候已经绑定到了监听套接字socetfd所描述的对象上,accept函数新创建的socket对象其实并没有进行端口的占有,而是复制了socetfd的本地IP和端口号,并且记录了连接过来的客户端的IP和端口号

那么,当客户端发送数据过来的时候,究竟是与哪一个socket对象通信呢   

客户端发送过来的数据可以分为2种,一种是连接请求,一种是已经建立好连接后的数据传输。

由于TCP/IP协议栈是维护着一个接收和发送缓冲区的。在接收到来自客户端的数据包后,服务器端的TCP/IP协议栈应该会做如下处理:如果收到的是请求连接的数据包,则传给监听着连接请求端口的socetfd套接字,进行accept处理;如果是已经建立过连接后的客户端数据包,则将数据放入接收缓冲区。这样,当服务器端需要读取指定客户端的数据时,则可以利用socketfd_new 套接字通过recv或者read函数到缓冲区里面去取指定的数据(因为socketfd_new代表的socket对象记录了客户端IP和端口,因此可以鉴别)。

如果一个程序创建了一个socket,并让其监听80端口,其实是向TCP/IP协议栈声明了其对80端口的占有。以后,所有目标是80端口的TCP数据包都会转发给该程序(这里的程序,因为使用的是Socket编程接口,所以首先由Socket层来处理)。所谓accept函数,其实抽象的是TCP的连接建立过程。accept函数返回的新socket其实指代的是本次创建的连接,而一个连接是包括两部分信息的,一个是源IP和源端口,另一个是宿IP和宿端口。所以,accept可以产生多个不同的socket,而这些socket里包含的宿IP和宿端口是不变的,变化的只是源IP和源端口。这样的话,这些socket宿端口就可以都是80,而Socket层还是能根据源/宿对来准确地分辨出IP包和socket的归属关系,从而完成对TCP/IP协议的操作封装! 

 

      accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字。

此时我们需要区分两种套接字,

       监听套接字: 监听套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,是服务器开始调用socket()函数生成的,称为监听socket描述字(监听套接字)

       连接套接字:一个套接字会从主动连接的套接字变身为一个监听套接字;而accept函数返回的是已连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。

       一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

        自然要问的是:为什么要有两种套接字?原因很简单,如果使用一个描述字的话,那么它的功能太多,使得使用很不直观,同时在内核确实产生了一个这样的新的描述字。

连接套接字socketfd_new并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd一样的端口号


本文部分转自http://blog.51cto.com/ticktick/779866