首页 > 代码库 > C++ Socket 学习笔记

C++ Socket 学习笔记

Socket学习笔记

以下均为整理,做参考之用。


IP Address

IP地址是指互联网协议地址(英语:Internet Protocol Address,又译为网际协议地址),是IP Address的缩写。IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。

IP地址被用来给Internet上的电脑一个编号。大家日常见到的情况是每台联网的PC上都需要有IP地址,才能正常通信。我们可以把“个人电脑”比作“一台电话”,那么“IP地址”就相当于“电话号码”,而Internet中的路由器,就相当于电信局的“程控式交换机”。

IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)。IP地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。例:点分十进IP地址(100.4.5.6),实际上是32位二进制数(01100100.00000100.00000101.00000110)。

每台计算机都有自己独一无二的IP地址,根据IP地址判断与哪台计算机进行通信。

Port

一台计算机可以同时提供多种网络服务,例如Web服务、FTP服务(文件传输服务)、SMTP服务(邮箱服务)等,仅有 IP 地址,计算机虽然可以正确接收到数据包,但是却不知道要将数据包交给哪个网络程序来处理,所以通信失败。

为了区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号(Port Number),例如,Web服务的端口号是 80,FTP 服务的端口号是 21,SMTP 服务的端口号是 25。

端口(Port)是一个虚拟的、逻辑上的概念。可以将端口理解为一道门,数据通过这道门流入流出,每道门有不同的编号,就是端口号。

Protocol

Protocol为进行网络中的数据交换而建立的规则、标准或约定。用于不同系统中实体间的通信。两个实体要想通信,必须有“同一种语言”,而且,对于通信内容,怎样通信和何时通信,都必须遵守一定的规定,这些规定就是协议。亦可简单地定义为:控制两实体间数据交换的一套规则。在电子通讯连接中,各个不同的层次都有自己的协议。

协议仅仅是一种规范,实现由计算机软件来完成,常见的协议TCP/IP协议:是目前世界上应用最为广泛的协议,TCP/IP协议是传输层的协议,HTTP超文本传输协议,FTP文件传输协议,SMTP简单邮件传送协议,Telnet远程登录服务是应用层协议。

TCP/IP、Http、Socket的区别

网络由下往上分为:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。

IP协议对应于网络层,TCP协议对应于传输层,而HTTP协议对应于应用层,三者从本质上来说没有可比性,socket则是对TCP/IP协议的封装和应用(程序员层面上)。也可以说,TPC/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。

简单的来说,当应用程序产生数据需要传输时,首先使用HTTP协议封装成文本信息,然后使用TCP/IP协议进行网络上传输,而Socket则是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API)。通过Socket,我们才能使用TCP/IP协议。从而形成了一些最基本的函数接口,比如create、listen、connect、accept、send、read和write等等。

TCP是什么?

具体的关于TCP是什么,我不打算详细的说了;当你看到这篇文章时,我想你也知道TCP的概念了,想要更深入的了解TCP的工作,我们就继续。它只是一个超级麻烦的协议,而它又是互联网的基础,也是每个程序员必备的基本功。首先来看看OSI的七层模型:
技术分享

我们需要知道TCP工作在网络OSI的七层模型中的第四层——Transport层,IP在第三层——Network层,ARP在第二层——Data Link层;在第二层上的数据,我们把它叫Frame,在第三层上的数据叫Packet,第四层的数据叫Segment。 同时,我们需要简单的知道,数据从应用层发下来,会在每一层都会加上头部信息,进行封装,然后再发送到数据接收端。这个基本的流程你需要知道,就是每个数据都会经过数据的封装和解封装的过程。 在OSI七层模型中,每一层的作用和对应的协议如下:

技术分享

TCP是一个协议,那这个协议是如何定义的,它的数据格式是什么样子的呢?要进行更深层次的剖析,就需要了解,甚至是熟记TCP协议中每个字段的含义。

技术分享

上面就是TCP协议头部的格式,由于它太重要了,是理解其它内容的基础,下面就将每个字段的信息都详细的说明一下。

  • Source Port和Destination Port:分别占用16位,表示源端口号和目的端口号;用于区别主机中的不同进程,而IP地址是用来区分不同的主机的,源端口号和目的端口号配合上IP首部中的源IP地址和目的IP地址就能唯一的确定一个TCP连接;
  • Sequence Number:用来标识从TCP发端向TCP收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节在数据流中的序号;主要用来解决网络报乱序的问题;
  • Acknowledgment Number:32位确认序列号包含发送确认的一端所期望收到的下一个序号,因此,确认序号应当是上次已成功收到数据字节序号加1。不过,只有当标志位中的ACK标志(下面介绍)为1时该确认序列号的字段才有效。主要用来解决不丢包的问题;
  • Offset:给出首部中32 bit字的数目,需要这个值是因为任选字段的长度是可变的。这个字段占4bit(最多能表示15个32bit的的字,即4*15=60个字节的首部长度),因此TCP最多有60字节的首部。然而,没有任选字段,正常的长度是20字节;
  • TCP Flags:TCP首部中有6个标志比特,它们中的多个可同时被设置为1,主要是用于操控TCP的状态机的,依次为URG,ACK,PSH,RST,SYN,FIN。每个标志位的意思如下:

    • URG:此标志表示TCP包的紧急指针域(后面马上就要说到)有效,用来保证TCP连接不被中断,并且督促中间层设备要尽快处理这些数据;
    • ACK:此标志表示应答域有效,就是说前面所说的TCP应答号将会包含在TCP数据包中;有两个取值:0和1,为1的时候表示应答域有效,反之为0;
    • PSH:这个标志位表示Push操作。所谓Push操作就是指在数据包到达接收端以后,立即传送给应用程序,而不是在缓冲区中排队;
    • RST:这个标志表示连接复位请求。用来复位那些产生错误的连接,也被用来拒绝错误和非法的数据包;
    • SYN:表示同步序号,用来建立连接。SYN标志位和ACK标志位搭配使用,当连接请求的时候,SYN=1,ACK=0;连接被响应的时候,SYN=1,ACK=1;这个标志的数据包经常被用来进行端口扫描。扫描者发送一个只有SYN的数据包,如果对方主机响应了一个数据包回来 ,就表明这台主机存在这个端口;但是由于这种扫描方式只是进行TCP三次握手的第一次握手,因此这种扫描的成功表示被扫描的机器不很安全,一台安全的主机将会强制要求一个连接严格的进行TCP的三次握手;
    • FIN: 表示发送端已经达到数据末尾,也就是说双方的数据传送完成,没有数据可以传送了,发送FIN标志位的TCP数据包后,连接将被断开。这个标志的数据包也经常被用于进行端口扫描。
      Window:窗口大小,也就是有名的滑动窗口,用来进行流量控制;这是一个复杂的问题,这篇博文中并不会进行总结的;

TCP连接和断开

技术分享

三次握手

  1. 第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;

  2. 第二次握手:服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;

  3. 第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。

既然总结了TCP的三次握手,那为什么非要三次呢?怎么觉得两次就可以完成了。那TCP为什么非要进行三次连接呢?在谢希仁的《计算机网络》中是这样说的:

为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

在书中同时举了一个例子,如下:

“已失效的连接请求报文段”的产生在这样一种情况下:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。”

四次分手

  1. 第一次分手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;

  2. 第二次分手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;

  3. 第三次分手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;

  4. 第四次分手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。

那四次分手又是为何呢?TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。

出处

socket 函数

套接字是通信端点的抽象,实现端对端之间的通信。与应用程序要使用文件描述符访问文件一样,访问套接字需要套接字描述符。任何套接字编程都必须调用socket 函数获得套接字描述符,这样才能对套接字进行操作。以下是该函数的描述:


    /* 套接字 */  

    /* 
     * 函数功能:创建套接字描述符; 
     * 返回值:若成功则返回套接字非负描述符,若出错返回-1; 
     * 函数原型: 
     */  
    #include <sys/socket.h>  

    int socket(int family, int type, int protocol);  
    /* 
     * 说明: 
     * socket类似与open对普通文件操作一样,都是返回描述符,后续的操作都是基于该描述符; 
     * family 表示套接字的通信域,不同的取值决定了socket的地址类型,其一般取值如下: 
     * (1)AF_INET         IPv4因特网域 
     * (2)AF_INET6        IPv6因特网域 
     * (3)AF_UNIX         Unix域 
     * (4)AF_ROUTE        路由套接字 
     * (5)AF_KEY          密钥套接字 
     * (6)AF_UNSPEC       未指定 
     * 
     * type确定socket的类型,常用类型如下: 
     * (1)SOCK_STREAM     有序、可靠、双向的面向连接字节流套接字 
     * (2)SOCK_DGRAM      长度固定的、无连接的不可靠数据报套接字 
     * (3)SOCK_RAW        原始套接字 
     * (4)SOCK_SEQPACKET  长度固定、有序、可靠的面向连接的有序分组套接字 
     * 
     * protocol指定协议,常用取值如下: 
     * (1)0               选择type类型对应的默认协议 
     * (2)IPPROTO_TCP     TCP传输协议 
     * (3)IPPROTO_UDP     UDP传输协议 
     * (4)IPPROTO_SCTP    SCTP传输协议 
     * (5)IPPROTO_TIPC    TIPC传输协议 
     * 
     */  

connect 函数

在处理面向连接的网络服务时,例如 TCP ,交换数据之前必须在请求的进程套接字和提供服务的进程套接字之间建立连接。TCP 客户端可以调用函数connect 来建立与 TCP 服务器端的一个连接。该函数的描述如下:


    /* 
     * 函数功能:建立连接,即客户端使用该函数来建立与服务器的连接; 
     * 返回值:若成功则返回0,出错则返回-1; 
     * 函数原型: 
     */  
    #include <sys/socket.h>  

    int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);  
    /* 
     * 说明: 
     * sockfd是系统调用的套接字描述符,即由socket函数返回的套接字描述符; 
     * servaddr是目的套接字的地址,该套接字地址结构必须包含目的IP地址和目的端口号,即想与之通信的服务器地址; 
     * addrlen是目的套接字地址的大小; 
     * 
     * 如果sockfd没有绑定到一个地址,connect会给调用者绑定一个默认地址,即内核会确定源IP地址,并选择一个临时端口号作为源端口号; 
     */  

TCP 客户端在调用函数 connect 前不必非得调用 bind 函数,因为内核会确定源 IP 地址,并选择一个临时端口作为源端口号。若 TCP 套接字调用connect 函数将建立 TCP 连接(执行三次握手),而且仅在连接建立成功或出错时才返回,其中出错返回可能有以下几种情况:

  • 若 TCP 客户端没有收到 SYN 报文段的响应,则返回 ETIMEOUT 错误;

  • 若客户端的 SYN 报文段的响应是 RST (表示复位),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接。只是一种硬错误,客户端一接收到 RST 就立即返回ECONNERFUSED 错误; RST 是 TCP 在发生错误时发送的一种 TCP 报文段。产生 RST 的三个条件时:

    • 目的地为某端口的 SYN 到达,然而该端口上没有正在监听的服务器;
    • TCP 想取消一个已有连接;
    • TCP 接收到一个不存在的连接上的报文段;
  • 若客户端发出的 SYN 在中某个路由器上引发一个目的地不可达的 ICMP 错误,这是一个软错误。客户端主机内核保存该消息,并在一定的时间间隔继续发送 SYN (即重发)。在某规定的时间后仍未收到响应,则把保存的消息(即 ICMP 错误)作为EHOSTUNREACH 或ENETUNREACH 错误返回给进行。

bind 函数

调用函数 socket 创建套接字描述符时,该套接字描述符是存储在它的协议族空间中,没有具体的地址,要使它与一个地址相关联,可以调用函数 bind 使其与地址绑定。客户端的套接字关联的地址一般可由系统默认分配,因此不需要指定具体的地址。若要为服务器端套接字绑定地址,可以通过调用函数 bind 将套接字绑定到一个地址。下面是该函数的描述:

   /* 套接字的基本操作 */  

    /* 
     * 函数功能:将协议地址绑定到一个套接字;其中协议地址包含IP地址和端口号; 
     * 返回值:若成功则返回0,若出错则返回-1; 
     * 函数原型: 
     */  
    #include <sys/socket.h>  
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);  
    /* 
     * 说明: 
     * sockfd 为套接字描述符; 
     * addr是一个指向特定协议地址结构的指针; 
     * addrlen是地址结构的长度; 
     */  

对于 TCP 协议,调用 bind 函数可以指定一个端口号,或指定一个 IP 地址,也可以两者都指定,还可以都不指定。若 TCP 客户端或服务器端不调用bind 函数绑定一个端口号,当调用connect 或 listen 函数时,内核会为相应的套接字选择一个临时端口号。一般 TCP 客户端使用内核为其选择一个临时的端口号,而服务器端通过调用bind 函数将端口号与相应的套接字绑定。进程可以把一个特定的 IP 地址捆绑到它的套接字上,但是这个 IP 地址必须属于其所在主机的网络接口之一。对于 TCP 客户端,这就为在套接字上发送的 IP 数据报指派了源 IP 地址。对于 TCP 服务器端,这就限定该套接字只接收那些目的地为这个 IP 地址的客户端连接。TCP 客户端一般不把 IP 地址捆绑到它的套接字上。当连接套接字时,内核将根据所用外出网络接口来选择源 IP 地址,而所用外出接口则取决于到达服务器端所需的路径。若 TCP 服务器端没有把 IP 地址捆绑到它的套接字上,内核就把客户端发送的 SYN 的目的 IP 地址作为服务器端的源 IP 地址。

在地址使用方面有下面一些限制:

  • 在进程所运行的机器上,指定的地址必须有效,不能指定其他机器的地址;
  • 地址必须和创建套接字时的地址族所支持的格式相匹配;
  • 端口号必须不小于1024,除非该进程具有相应的特权(超级用户);
  • 一般只有套接字端点能够与地址绑定,尽管有些协议允许多重绑定;

listen 函数

在编写服务器程序时需要使用监听函数 listen 。服务器进程不知道要与谁连接,因此,它不会主动地要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后响应该连接请求,并对它做出处理,一个服务进程可以同时处理多个客户进程的连接。listen 函数描述如下:

  /* 
     * 函数功能:接收连接请求; 
     * 函数原型: 
     */  
    #include <sys/socket.h>  

    int listen(int sockfd, int backlog);//若成功则返回0,若出错则返回-1;  
    /* 
     * sockfd是套接字描述符; 
     * backlog是该进程所要入队请求的最大请求数量; 
     */  

listen 函数仅由 TCP 服务器调用,它有以下两种作用:

  • 当 socket 函数创建一个套接字时,若它被假设为一个主动套接字,即它是一个将调用connect 发起连接的客户端套接字。listen 函数把一个未连接的套接字转换成一个被动套接字,指示内核应该接受指向该套接字的连接请求;
  • listen 函数的第二个参数规定内核应该为相应套接字排队的最大连接个数;

listen 函数一般应该在调用socket 和bind 这两个函数之后,并在调用accept 函数之前调用。 内核为任何一个给定监听套接字维护两个队列:

  • 未完成连接队列,每个这样的 SYN 报文段对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的 TCP 三次握手过程。这些套接字处于 SYN_REVD 状态;
  • 已完成连接队列,每个已完成 TCP 三次握手过程的客户端对应其中一项。这些套接字处于 ESTABLISHED 状态;

accept 函数

accept 函数由 TCP 服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。该函数的返回值是一个新的套接字描述符,返回值是表示已连接的套接字描述符,而第一个参数是服务器监听套接字描述符。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示 TCP 三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。该函数描述如下:

   /* 函数功能:从已完成连接队列队头返回下一个已完成连接;若已完成连接队列为空,则进程进入睡眠; 
     * 函数原型: 
     */  
    int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);//返回值:若成功返回套接字描述符,出错返回-1;  
    /* 
     * 说明: 
     * 参数 cliaddr 和 addrlen 用来返回已连接的对端(客户端)的协议地址; 
     * 
     * 该函数返回套接字描述符,该描述符连接到调用connect函数的客户端; 
     * 这个新的套接字描述符和原始的套接字描述符sockfd具有相同的套接字类型和地址族,而传给accept函数的套接字描述符sockfd没有关联到这个链接, 
     * 而是继续保持可用状态并接受其他连接请求; 
     * 若不关心客户端协议地址,可将cliaddr和addrlen参数设置为NULL,否则,在调用accept之前,应将参数cliaddr设为足够大的缓冲区来存放地址, 
     * 并且将addrlen设为指向代表这个缓冲区大小的整数指针; 
     * accept函数返回时,会在缓冲区填充客户端的地址并更新addrlen所指向的整数为该地址的实际大小; 
     * 
     * 若没有连接请求等待处理,accept会阻塞直到一个请求到来; 
     */  

fork 和 exec 函数


    /* 函数功能:创建子进程; 
     * 返回值: 
     * (1)在子进程中,返回0; 
     * (2)在父进程中,返回新创建子进程的进程ID; 
     * (3)若出错,则范回-1; 
     * 函数原型: 
     */  
    #include <unistd.h>  
    pid_t fork(void);  
    /* 说明: 
     * 该函数调用一次若成功则返回两个值: 
     * 在调用进程(即父进程)中,返回新创建进程(即子进程)的进程ID; 
     * 在子进程返回值是0; 
     * 因此,可以根据返回值判断进程是子进程还是父进程; 
     */  

    /* exec 序列函数 */  

    /* 
     * 函数功能:把当前进程替换为一个新的进程,新进程与原进程ID相同; 
     * 返回值:若出错则返回-1,若成功则不返回; 
     * 函数原型: 
     */  
    #include <unistd.h>  
    int execl(const char *pathname, const char *arg, ...);  
    int execv(const char *pathnam, char *const argv[]);  
    int execle(const char *pathname, const char *arg, ... , char *const envp[]);  
    int execve(const char *pathnam, char *const argv[], char *const envp[]);  
    int execlp(const char *filename, const char *arg, ...);  
    int execvp(const char *filename, char *const argv[]);  
    /*  6 个函数的区别如下: 
     * (1)待执行的程序文件是 文件名 还是由 路径名 指定; 
     * (2)新程序的参数是 一一列出 还是由一个 指针数组 来引用; 
     * (3)把调用进程的环境传递给新程序 还是 给新程序指定新的环境; 
     */  

exec 6个函数在函数名和使用语法的规则上都有细微的区别,下面就从可执行文件查找方式、参数传递方式及环境变量这几个方面进行比较。

  • 查找方式:前4个函数的查找方式都是完整的文件目录路径 pathname ,而最后两个函数(也就是以p结尾的两个函数)可以只给出文件名 filename,系统就会自动按照环境变量 “$PATH” 所指定的路径进行查找。
  • 参数传递方式:exec 序列函数的参数传递有两种方式:一种是逐个列举的方式,而另一种则是将所有参数整体构造指针数组传递。在这里是以函数名的第5位字母来区分的,字母为 “l”(list)的表示逐个列举参数的方式,其语法为 const char *arg;字母为 “v”(vertor)的表示将所有参数整体构造指针数组传递,其语法为 char *const argv[]。读者可以观察 execl()、execle()、execlp() 的语法与 execv()、execve()、execvp() 的区别。这里的参数实际上就是用户在使用这个可执行文件时所需的全部命令选项字符串(包括该可执行程序命令本身)。要注意的是,这些参数必须以NULL结束。
  • 环境变量:exec 序列函数可以默认系统的环境变量,也可以传入指定的环境变量。这里以 “e”(environment)结尾的两个函数 execle() 和 execve() 就可以在 envp[] 中指定当前进程所使用的环境变量。

并发服务器

当要求一个服务器同时为多个客户服务时,需要并发服务器。TCP 并发服务器,它们为每个待处理的客户端连接调用 fork 函数派生一个子进程。当一个连接建立时,accept 返回,服务器接着调用 fork 函数,然后由子进程服务客户端,父进程则等待另一个连接,此时,父进程必须关闭已连接套接字。

close 和 shutdown 函数

当要关闭套接字时,可使用 close 和 shutdown 函数,其描述如下:

 /* 函数功能:关闭套接字,若是在 TCP 协议中,并终止 TCP 连接; 
     * 返回值:若成功则返回0,若出错则返回-1; 
     * 函数原型: 
     */  
    #include <unistd.h>  
    int close(int sockfd);  

    /* 
     * 函数功能:关闭套接字上的输入或输出; 
     * 返回值:若成功则返回0,若出错返回-1; 
     * 函数原型: 
     */  
    #include <sys/socket.h>  
    int shutdown(int sockfd, int how);  
    /* 
     * 说明: 
     * sockfd表示待操作的套接字描述符; 
     * how表示具体操作,取值如下: 
     * (1)SHUT_RD     关闭读端,即不能接收数据 
     * (2)SHUT_WR     关闭写端,即不能发送数据 
     * (3)SHUT_RDWR   关闭读、写端,即不能发送和接收数据 
     * 
     */  

getsockname 和 getpeername 函数

为了获取已绑定到套接字的地址,我们可以调用函数 getsockname 来实现:


    /* 
     * 函数功能:获取已绑定到一个套接字的地址; 
     * 返回值:若成功则返回0,若出错则返回-1; 
     * 函数原型: 
     */  
    #include <sys/socket.h>  

    int getsockname(int sockfd, struct sockaddr *addr, socklen_t *alenp);  
    /* 
     * 说明: 
     * 调用该函数之前,设置alenp为一个指向整数的指针,该整数指定缓冲区sockaddr的大小; 
     * 返回时,该整数会被设置成返回地址的大小,如果该地址和提供的缓冲区长度不匹配,则将其截断而不报错; 
     */  
    /* 
     * 函数功能:获取套接字对方连接的地址; 
     * 返回值:若成功则返回0,若出错则返回-1; 
     * 函数原型: 
     */  
    #include <sys/socket.h>  

    int getpeername(int sockfd, struct sockaddr *addr, socklen_t *alenp);  
    /* 
     * 说明: 
     * 该函数除了返回对方的地址之外,其他功能和getsockname一样; 
     */  

出处


测试实例

客户端

#include <iostream>
#include <cstdio>
#include <Winsock2.h>

using namespace std;

#pragma comment(lib, "ws2_32.lib")  

#define xPort 8000
#define xIP "127.0.0.1"

int main()
{
    const int xBUF_SIZE = 64;
    WSADATA         wsd;                //WSADATA变量
    SOCKET          xServer;            //服务器套接字
    SOCKET          xClient;            //客户端套接字
    SOCKADDR_IN     xaddrServ;          //服务器地址

    char            recBuf[xBUF_SIZE];  //接受数据缓冲区
    char            sendBuf[xBUF_SIZE]; //返回数据缓冲区
    int             retVal;             //返回值

    //初始化套结字动态库,请求2.2版本winsock
    if (WSAStartup(MAKEWORD(2,2), &wsd) != 0)  
    {  
        cout << "WSAStartup failed!" << endl;  
        return 1;  
    } 

    /*
    //创建socket操作
    // SOCKET socket(int af, int type, int protocol);  
    // 第一个参数,指定地址簇(TCP/IP只能是AF_INET,也可写成PF_INET)  
    // 第二个,选择套接字的类型(流式套接字),第三个,特定地址家族相关协议(0为自动)  
    */

//创建套接字
    xClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(INVALID_SOCKET == xClient)
    {
        cout << "socket failed!" << endl;  
        WSACleanup();//释放套接字资源;  
        return  -1;
    }

//服务器套接字地址
    xaddrServ.sin_family = AF_INET;
    xaddrServ.sin_port = htons(xPort);
    xaddrServ.sin_addr.s_addr = inet_addr(xIP);

    /*
    // 将套接字xClient与远程主机相连  
    // int connect( SOCKET s,  const struct sockaddr* name,  int namelen);  
    // 第一个参数:需要进行连接操作的套接字  
    // 第二个参数:设定所需要连接的地址信息  
    // 第三个参数:地址的长度
    */
//连接服务器
    retVal = connect(xClient, (LPSOCKADDR)&xaddrServ, sizeof(xaddrServ));
    if(SOCKET_ERROR == retVal)  
    {  
        cout << "connect failed!" << endl;  
        closesocket(xClient); //关闭套接字  
        WSACleanup(); //释放套接字资源  
        return -1;  
    } 
    ///三次握手完成

// 客户端与用户端进行通信  
    /*
    // send(), 在套接字上发送数据  
    // int send( SOCKET s,  const char* buf,  int len,  int flags);  
    // 第一个参数,需要发送信息的套接字,  
    // 第二个参数,包含了需要被传送的数据,  
    // 第三个参数是buffer的数据长度,  
    // 第四个参数,一些传送参数的设置  

    // recv(), 在套接字上接收数据  
    // int recv(  SOCKET s,  char* buf,  int len,  int flags);  
    // 第一个参数,建立连接后的套接字,  
    // 第二个参数,接收数据  
    // 第三个参数,接收数据的长度,  
    // 第四个参数,一些传送参数的设置
    */
    cout << "*************************客户端**********************"<< endl;
    while(true)
    {
        memset(sendBuf, ‘/0‘, xBUF_SIZE);  
        cout << "向服务器发送数据:";
        cin >> sendBuf;
        retVal = send(xClient, sendBuf, xBUF_SIZE, 0);
        if (SOCKET_ERROR == retVal)  
        {  
            cout << "send failed!" << endl;        
            closesocket(xClient);   //关闭套接字       
            WSACleanup();           //释放套接字资源;  
            return -1;  
        }  
        if(sendBuf[0] == ‘0‘)  
            break;  
        memset(recBuf, ‘/0‘, sizeof(recBuf));
        recv(xClient, recBuf, xBUF_SIZE , 0); // 接收服务器端的数据, 只接收5个字符  
        cout <<"从服务器接收数据:"<< recBuf << endl;  
        cout<<endl;   
    }

    //退出  
    closesocket(xClient);   //关闭套接字  
    WSACleanup();           //释放套接字资源;  

    return 0;
}

服务器

#include <iostream>
#include <cstdio>
#include <Winsock2.h>
#include <cstring>
using namespace std;

#pragma comment(lib, "ws2_32.lib")  

#define xPort 8000
#define xIP "127.0.0.1"

int main()
{
    const int xBUF_SIZE = 64;
    WSADATA         wsd;                //WSADATA变量
    SOCKET          xServer;            //服务器套接字
    SOCKET          xClient;                //客户端套接字
    SOCKADDR_IN     xaddrServ;          //服务器地址

    char            recBuf[xBUF_SIZE];  //接受数据缓冲区
    char            sendBuf[xBUF_SIZE]; //返回数据缓冲区
    int             retVal;             //返回值

    //初始化套结字动态库,请求2.2版本winsock
    if (WSAStartup(MAKEWORD(2,2), &wsd) != 0)  
    {  
        cout << "WSAStartup failed!" << endl;  
        return 1;  
    } 

    /*
    //创建socket操作
    // SOCKET socket(int af, int type, int protocol);  
    // 第一个参数,指定地址簇(TCP/IP只能是AF_INET,也可写成PF_INET)  
    // 第二个,选择套接字的类型(流式套接字),第三个,特定地址家族相关协议(0为自动)  
    */

//创建套接字
    xServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(INVALID_SOCKET == xServer)
    {
        cout << "socket failed!" << endl;  
        WSACleanup();//释放套接字资源;  
        return  -1;
    }

//服务器套接字地址
    xaddrServ.sin_family = AF_INET;
    xaddrServ.sin_port = htons(xPort);
    xaddrServ.sin_addr.s_addr = INADDR_ANY;

    /*
    //套接字xServer与本地地址相连  
    // int bind(SOCKET s, const struct sockaddr* name, int namelen);  
    // 第一个参数,指定需要绑定的套接字;  
    // 第二个参数,指定该套接字的本地地址信息,该地址结构会随所用的网络协议的不同而不同  
    // 第三个参数,指定该网络协议地址的长度  
    */

//绑定套接字
    retVal = bind(xServer, (SOCKADDR *)&xaddrServ, sizeof(SOCKADDR_IN));
    if(SOCKET_ERROR == retVal)
    {
        cout << "bind failed!" << endl;  
        closesocket(xServer);   //关闭套接字  
        WSACleanup();           //释放套接字资源;  
        return -1;  
    }

    /*
    // 将套接字设置为监听模式(连接请求), listen()通知TCP服务器准备好接收连接  
    // int listen(SOCKET s,  int backlog);  
    // 第一个参数指定需要设置的套接字,第二个参数为(等待连接队列的最大长度) 
    */

//开始监听   
    retVal = listen(xServer, 1);  
    if(SOCKET_ERROR == retVal)  
    {  
        cout << "listen failed!" << endl;         
        closesocket(xServer);   //关闭套接字  
        WSACleanup();           //释放套接字资源;  
        return -1;  
    }  

    /*
    // accept(),接收连接,等待客户端连接  
    // SOCKET accept(  SOCKET s,  struct sockaddr* addr,  int* addrlen);  
    // 第一个参数,接收一个处于监听状态下的套接字  
    // 第二个参数,sockaddr用于保存客户端地址的信息  
    // 第三个参数,用于指定这个地址的长度  
    // 返回的是向与这个监听状态下的套接字通信的套接字  
    */

//接受客户端请求  
    sockaddr_in addrClient;
    int addrClientLen = sizeof(addrClient);
    xClient = accept(xServer, (sockaddr FAR*)&addrClient, &addrClientLen);
    if(INVALID_SOCKET == xClient)  
    {  
        cout << "accept failed!" << endl;         
        closesocket(xServer);   //关闭套接字  
        WSACleanup();           //释放套接字资源;  
        return -1;  
    }  

    ///三次握手完成

// 客户端与用户端进行通信  
    /*
    // send(), 在套接字上发送数据  
    // int send( SOCKET s,  const char* buf,  int len,  int flags);  
    // 第一个参数,需要发送信息的套接字,  
    // 第二个参数,包含了需要被传送的数据,  
    // 第三个参数是buffer的数据长度,  
    // 第四个参数,一些传送参数的设置  

    // recv(), 在套接字上接收数据  
    // int recv(  SOCKET s,  char* buf,  int len,  int flags);  
    // 第一个参数,建立连接后的套接字,  
    // 第二个参数,接收数据  
    // 第三个参数,接收数据的长度,  
    // 第四个参数,一些传送参数的设置
    */
    cout << "*************************服务器**********************"<< endl;
    while(true)
    {
        memset(recBuf, ‘/0‘, xBUF_SIZE);  
        retVal = recv(xClient, recBuf, xBUF_SIZE, 0);
        if (SOCKET_ERROR == retVal)  
        {  
            cout << "recv failed!" << endl;       
            closesocket(xServer);   //关闭套接字  
            closesocket(xClient);   //关闭套接字       
            WSACleanup();           //释放套接字资源;  
            return -1;  
        }  
        if(recBuf[0] == ‘0‘)  
            break;  
        cout << "客户端发送的数据: " << recBuf <<endl;  

        memset(sendBuf, ‘/0‘, xBUF_SIZE);
        cout << "向客户端发送数据: " ;  
        cin >> sendBuf; 
        cout << endl;

        send(xClient, sendBuf, xBUF_SIZE, 0); 
    }

    //退出  
    closesocket(xServer);   //关闭套接字  
    closesocket(xClient);   //关闭套接字  
    WSACleanup();           //释放套接字资源;  

    return 0;
}
<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    C++ Socket 学习笔记