socket通信

这一篇博客是继上次的如何写一个简单的Web Server(一)的第二篇博客,拖了久,一直没有写,上个周把套接字通信的代码重构了一下,这周就打算把挖的坑填完。

Socket

这是维基百科上对socket的定义,socket中文叫套接字,说实话我觉得这个翻译不是很好,没有接触之前无法将套接字与网络编程联系起来;

根据维基的定义,socket主要用于端对端的网络通信,两个端系统上的应用程序通过两个进程对组成,从一个进程向另一个进程发发送报文必须通过套接字,我们可以把进程想象成两座房子,而它们的套接字相当于它们的门,当一个主机向另一个主机发送报文时,它需要将报文推出门(套接字),该发送进程假设门与另一侧之间有运输的基础设施,当报文抵达目地主机时,它通过接收进程的门(套接字)传递,然后接收进程对报文进行处理。

多路复用和多路分解

在了解了套接字的定义后,自然而然的会想到在通信的过程中两个通信进程之间如何识别对方的问题,这就涉及到运输层的多路分解和多路复用;在端系统上运行的进程有一个或者多个进程,那么如何标示一个特定的套接字显然是一个问题,在接收主机中从运输层来的数据没有直接交付给进程,而是通过一个中间的套接字来传递。由于在任一时刻接收主机上不止一个套接字,所以每个套接字都有唯一的标识符,标识符的格式取决于它是UDP套接字还是TCP套接字。

现在考虑接收主机如何将一个收到的运输层报文定向到合适的套接字,为了达到这一目的,在每个传输层报文中设置了几个字段,在接收端运输层检查这些字段来标识出接收的套接字,然后将报文定向到对应的套接字。将运输层报文交付到正确的套接字的工作称为多路分解;从源主机的不同套接字中收集数据块,并为每个数据块封装上首部信息(将在多路分解上使用)从而生成报文段,然后将报文段传递到网络层的过程叫做多路复用。如下图所示:

现在我们理解了运输层多路复与读多路分解的作用,现在我们以UDP的多路复用与多路分解为例子来看看在主机中它们实际上是怎样工作的,通过先前的内容我们知道了运输层多路复用的要求:
1. socket要有唯一的标识符
2. 每个报文端有特殊的字段来指示该报文段所要交付的套接字(如下图所示这些特殊的字段是源端口号字段和目地端口字段)

如上图所示,端口号由一个16比特的的数字,其大小在065535之间,01023范围的端口号称为周知端口号,它们是保留给诸如HTTP(端口号80)和FTP(端口号21)之类的周知应用层协议的,当我们开发一个新的应用程序时,必须为其分配一个端口号。

当报文段到达主机时,运输层检查报文段中的目地端口号,并将其定向到相应的套接字,然后报文段的数据通过套接字进入其连接的进程。下面我们通过完成一个通信的程序来看看整个通信的过程。

这个程序分为client和server端,这里对server段的代码进行介绍,client的程序不做介绍,以下是整个过程的示意图:

首先,我们考虑,如果你需要和你写的server端的程序进行通信,你需要给server上的应用进程分配一个socket,并与其绑定,所以整个程序可以分为三步:
1. server创建套接字并与server绑定
2. server与client建立连接
3. server读取报文后关闭连接
首先我们来看看创建socket的程序:

//
// @Brief: Create a socket for communicate with client.
void Server :: CreateSocket()
{
  socket_file_description_ = socket(AF_INET,   SOCK_STREAM, 0);       
  error_handler_.CheckSocketCreatedOrNot(socket_file_description_);
}

其中socket()函数的是在sys/socket.h中声明的,其声明为:

int socket(int family, int type, int protocol)
若成功返回非负描述符,若出错则为-1
其中fanily参数指明了协议族, type参数指明socket类型,protocal参数可以设为下图中的某个常值,或者设为0,以选择所给定familytype组合的系统默认值,familytypeprotocol*的值如下图所示:

socket函数在成功时返回一个非负整数值,它与文件描述符类似,我们称它为套接字描述符,选择有效的type和protocol的组合来创建套接字,有效的组合如下图(部分组合):

创建好套接字后就需要将端口号和IP地址和套接字绑定,代码如下:

     //
    // @Brief: Set the server address, port number
    void Server :: SetServerAddress()
    {
        bzero((char*)&server_address_, sizeof(server_address_));
        // convert the port number from string of digits to an interger.
        port_number_ = atoi(argv_[1]);
        // must be AF_INET which contain a code for the address family.
        server_address_.sin_family = AF_INET;
        // INADDR_ANY will get the IP address on which server runs.
        server_address_.sin_addr.s_addr = INADDR_ANY;
        // convert the port numberin host byte order to network order.
        server_address_.sin_port = htons(port_number_);
    }

     //
    // @Brief: Bind the socket with server.
    void Server :: BindSocketWithServer()
    {
        int bind_flag = bind(socket_file_description_, (sockaddr*)
        (&server_address_), sizeof(server_address_));
        error_handler_.CheckBindOrNot(bind_flag);
        listen(socket_file_description_, 5);
    }

上述代码中利用bind函数将一个本地协议地址赋予一个socket,对于IP协议,协议地址是32为的IPv4或者128位的IPv6地址与16位的TCP或者UDP端口号的组合,我们来看一看bind函数:

*int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen)
绑定成功返回0,若出错返回-1

bind函数中第二个参数是指向特定协议的地址结构结构指针,第三个参数是该地址的长度,对于TCP来说,调用bind函数可以指定一个端口号,或者一个IP地址,也可以两者都指定,也可以都不指定:

  • 在服务器启动时会绑定他们众所周知端口,对于TCP客户或者服务器,如果bind的时候没有指定绑定端口,当调用connect或者listen的时候内核就会为相应的socket选择一个临时的端口。
  • 进程可以把一个特定的IP地址绑定到它的socket上,不过这个IP地址必须属于其所在的主机的网络接口之一,对于TCP客户,这就为其指定了源IP地址。对于TCP服务器,这就限定该socket只接受目地IP为该服务器主机的客户连接。但是对于TCP客户机来一般不将IP地址绑定到其socket上,当客户机连接socket时,内核根据所用外出网络的的接口来选择源IP,而所用外出接口则取决于到达服务器所需的路径,如果TCP服务器没有把IP地址绑定到它的socket上,内核就把客户机发送的SYN的目地IP地址作为服务器的源IP地址。

调用bind的时候可以知道指定IP地址或者端口,可以两者都指定,也可以两者都不指定,如下图所示(根据结果来设置sin_addr和sin_port或者sin6_addr和sin6_port)

对于IPv4来说,通配地址由常值INADDR_ANY来指定,其值一般为0,它告知内核去选择IP地址,如下面的语句:

    struct sockaddr_in server_address;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);

其中htonl是将主机序转换成网络序(如果需要查看更多关于这个函数的细节,请自行去查看这个函数的man page)

但是INADDR_ANY的值无论在主机序还是在网络序中,其值都是一样的,因此使用htonl并非必需的。

将端口或IP绑定好之后,server就需要在这个端口上监听连接了,监听需要用到listen函数,其声明如下:

int listen(int sockfd, int backlog)
若成功返回0,若出错返回-1

listen函数仅由TCP服务器调用,它做两件事:

  • 当socket被创建时候,他默认被假设为一个主动socket,即它被默认是一个将要调用connect函数的客户socket,而listen函数将一个未连接的socket转换成一个被动socket,指示内核应接受指向该socket的连接请求,根据TCP状态转换图,服务器的连接是被动打开的,状态由CLOSED转移到LISTEN。
  • 函数的第二个参数(backlog)规定了内核应该为相应socket排队的最大连接个数字;这里backlog参数我们做以下理解:
      1. 未完成连接队列,即处于SYN_RCVD的等待完成TCP三次握手过程的连接。
      1. 已完成连接队列,即已完成三次握手过程的连接,这些socket处于ESTABLISAHED状态。

以上这两个队列之和不超过backlog

剩下的就是client和server建立连接之后读写数据了,代码如下:

    //
    // @Brief: Establish connect with client.
    void Server :: EstablishConnect()
    {
        client_length_ = sizeof(client_address_);
        while(1)
        {
               //establish the connection
               new_socket_file_description_ = accept(socket_file_description_,
               (sockaddr*) &client_address_, &client_length_);
               error_handler_.CheckAcceptOrNot(new_socket_file_description_);
               pid_ = fork(); //create a new process to handle this connection
               if(pid_ < 0)
                    error_handler_.ErrorMessageDisplay("Error on fork");
               if(pid_ == 0)
               {
                  close(socket_file_description_);
                  DisplayMessageFromClient();
                  exit(0); // the process exits
                }else{ // the parent closes the new socket file description  
                       close(new_socket_file_description_);
                }
          }
    }

配套源码


参考文献:
unix网络编程
Sockets Tutorial


Keep focus and have fun

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容

  • 1)OSI与TCP/IP各层的结构与功能,都有哪些协议。 OSI分层 (7层):物理层、数据链路层、网络层、传输层...
    ldlywt阅读 2,309评论 0 26
  • socket通信原理 socket又被叫做套接字,它就像连接到两端的插座孔一样,通过建立管道,将两个不同的进程之间...
    jiodg45阅读 1,128评论 0 1
  • 前言 我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页时,浏览器的进程怎么与web服...
    Chars阅读 2,981评论 2 117
  • 一、网络各个协议:TCP/IP、SOCKET、HTTP等## 网络七层由下往上分别为物理层、数据链路层、网络层、传...
    Mighty_man阅读 443评论 0 2
  • 近期在做的项目中,涉及到了进程间数据传输,系统的原本实现是通过管道,但是原有的实现中两个进程是在同一台机器,而且两...
    Jensen95阅读 3,139评论 0 8