C/C++ 套接字编程 简单教程

发布于:2025-08-30 ⋅ 阅读:(20) ⋅ 点赞:(0)

一、概述
1、套接字(Socket),套接字通信是网络编程中的基础机制。

2、Windows和Linux平台套接字函数几乎一样,使用流程也几乎一样,区别如下:
  (1)、头文件与库依赖:
       Windows: 必须包含winsock2.h和ws2tcpip.h,并通过#pragma comment(lib, "ws2_32.lib")链接库文件。
       Linux: 使用标准头文件如sys/socket.h、netinet/in.h和arpa/inet.h,无需额外链接库。
  (2)、初始化和清理机制:
       Windows: 需显示调用WSAStartup()初始化Winsock库,结束时使用WSACleanup()清理资源。
       Linux: 无初始化要求,直接创建Socket即可使用。
  (3)、错误处理方式:
       Windows: 错误码通过WSAGetLastError()获取,需手动处理错误状态。
       Linux: 通过全局变量errno自动捕获错误码,简化调试流程。
  (4)、Socket描述符操作:
       Windows: Socket描述符定义为SOCKET类型(类似句柄),关闭时使用closesocket()函数。
       Linux: Socket描述符为int类型(文件描述符),关闭时调用close()函数。
  (5)、非阻塞模式设置:
       Windows: 通过ioctlsocket()函数设置非阻塞模式。
       Linux: 使用fcntl()或ioctl()实现相同功能。
  (6)、高性能I/O模型:
       Windows: 依赖IOCP(I/O完成端口)处理高并发,优化吞吐量。
       Linux: 采用epoll机制,支持高效的事件驱动模型。
  (7)、多路复用(如select函数):
       Windows: select()支持Socket和文件描述符混合操作。
       Linux: select()仅支持文件描述符,限制了灵活性。
  (8)、数据类型兼容性:
       Windows: 需自定义类型如socklen_t,以适配POSIX标准。
       Linux: 原生支持POSIX数据类型,减少移植复杂度。

3、C++中没有用于套接字通信的类,可以自己封装C语言的套接字API。

4、局域网和广域网:
  (1)、局域网: 局域网将一定区域内的各种计算机、外部设备、数据库等连接起来,形成计算机通信的私有网络。
  (2)、广域网: 又称外网、公网,是连接不同地区局域网或城域网计算机通信的远程公共网络。
  (3)、局域网连接外网的方式:
      a、局域网内有一台代理服务器,通过提供的账号登录到代理服务器,代理服务器帮助连接到外网。
      b、由路由器提供的局域网,路由器接上网线,也可以访问外网。
  (4)、每个国家都有防火墙,即使能够连接到外网,也不一定能访问某些网站。

5、IP:
  (1)、IP,Internet Protocol,本质是一个整形数,用于表示计算机在网络中的地址。
  (2)、IP有两个协议版本: IPv4和IPv6。
  (3)、IPv4(Internet Protocol version4):
      a、使用一个32位的整形数描述一个IP地址,4个字节。
      b、也可以使用一个点分十进制字符串描述IP地址,如: "192.168.247.135"。
      c、点分十进制字符串,分成了4份,每份1字节,8bit(char),最大值为255。
      d、按照IPv4协议计算,可以使用的IP地址共有2的32次方个。
  (4)、IPv6(Internet Protocol version6):
      a、使用一个128位的整形数描述一个IP地址,16个字节。
      b、也可以使用一个冒分十六进制字符串描述IP地址,如: "2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b"。
      c、字符串分成了8份,每份2字节,每一部分以16进制的方式表示。
      d、按照IPv6协议计算,可以使用的IP地址共有2的128次方个。
  (5)、查看IP地址:
      a、Linux平台:
         ifconfig 或者 ip addr
      b、Windows平台:
         ipconfig
  (6)、ping命令检测网络通信是否正常。
      a、ping ip,如: ping 192.168.1.101
      b、ping 域名,如: ping www.baidu.com
 
6、端口:
  (1)、通过IP地址可以定位到某一台主机,通过端口可以定位到主机上的某一进程。
  (2)、端口也是一个整形数unsigned short,一个16位整形数,有效端口的取值范围是: 0-65535。
  (3)、如果进程不需要网络通信,那么进程就不需要绑定端口。
  (4)、一个端口只能给某一个进程使用,多个进程不能同时使用同一个端口。

7、OSI/ISO网络分层模型:
  (1)、OSI,Open System Interconnect,即开放式系统互联,一般都叫OSI参考模型。
  (2)、OSI是ISO(国际标准化组织)在1985年研究的网络互联模型。
  (3)、OSI/ISO七层模型                    tcp/ip四层模型
         应用层 \ \ \ \ \ \ \ \
           |                     \ \ \ \ \
         表示层  -------------------------  应用层
           |                     / / / / /    |
         会话层  / / / / / / / /              |
           |                                  |
         传输层  -------------------------  传输层
           |                                  |
         网络层  -------------------------  网络互联层
           |                                  |
         数据链路层 \ \ \ \ \ \ \ \ \ \ \ \
           |                                网络接口层
         物理层     / / / / / / / / / / / /
  (4)、网络协议指的是计算机网络中互相通信的对等实体之间交换信息时所必须遵守的规则的集合。
  (5)、传输层协议: TCP协议、UDP协议。
  (6)、网络层协议: IP协议。
  (7)、网络接口层协议: 以太网帧协议。
  (8)、在网络通信时:
       a、程序员需要负责应用层数据的处理,应用层的数据可以使用协议(标准或自定义)进行封装,也可以不封装。
       b、程序员需要调用发送数据的接口函数,将数据发送出去。
       c、程序员调用的API做底层的数据处理(传输层使用传输层协议打包数据、网络层使用网络层协议打包数据、网络接口层使用网络接口层协议打包数据)。

8、套接字通信和编程语言没有关系,不同的语言实现的套接字程序之间,是可以通信的。

二、Socket编程
1、Socket套接字:
  (1)、Socket套接字由远景研究规划局(ARPA)资助加里福尼亚大学伯克利分校的一个研究组研发,其目的是将TCP/IP协议相关软件移植到类Unix系统中。
  (2)、设计者开发了一套接口,以便应用程序能简单地调用接口通信,这套接口不断完善,最终形成了Socket套接字。
  (3)、Linux系统采用了Socket套接字,因此Socket接口被广泛使用,已经成为事实上的标准。
  (4)、Socket对于程序员来说,就是一套网络通信的接口。
  (5)、网络通信主体分为两部分: 客户端和服务器端。
  (6)、Socket, 英文意思, 插座、插口。

2、字节序:
  (1)、在各种计算机体系结构中,由于存储机制有所不同,引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等)应该以什么样的顺序进行传送。
       如果不达成一致的规则,通信双方将无法进行正确的编/译码从而导致通信失败。
  (2)、字节序,顾名思义,字节的顺序,就是大于一个字节的类型(如int、float、long、double等)的数据在内存中的存放顺序。
       注意: 对于单字符来说,没有字节序问题,而字符串是单字符的集合,因此字符串也没有字节序问题。
  (3)、目前字节存储机制主要有两种: Big-Endian和Little-Endian。
  (4)、Big-Endian:
      a、大端字节序,一般也称网络字节序。
      b、数据的高位字节存储到内存的低地址位,数据的低位字节存储到内存的高地址位。
      c、套接字通信过程中操作的数据都是大端存储的,包括: 接收/发送的数据、IP地址、端口。
  (5)、Little-Endian:
      a、小端字节序,一般也称主机字节序。
      b、数据的低位字节存储到内存的低地址位,数据的高位字节存储到内存的高地址位。
      c、我们使用的PC机,数据的存储默认使用的是小端。
  (6)、举例: 0xab5c01ff
             内存低地址位     内存高地址位
      小端:  ff       01        5c       ab
      大端:  ab       5c        01       ff

3、IP地址转换:
  (1)、这套API主要用于网络通信中整形IP和端口的字节序转换。[整形IP不常用]

       #include <arpa/inet.h>
       uint16_t htons(uint16_t hostshort);  //将一个短整形从主机字节序转换到网络字节序
       uint32_t htonl(uint32_t hostlong);   //将一个整形从主机字节序转换到网络字节序
       uint16_t ntohs(uint16_t netshort);   //将一个短整型从网络字节序转换到主机字节序
       uint32_t ntohl(uint32_t netlong);    //将一个整形从网络字节序转换到主机字节序

  (2)、虽然IP地址本质是一个整形数,但在使用过程中常用字符串来描述。
  (3)、将字符串类型的IP地址进行大端转换(IPv4或IPv6): [推荐]
     a.int inet_pton(int af, const char * src, void * dst);   
       将主机字符串IP地址转换为整形网络字节序IP地址, af表示地址族(AF_INET是ipv4,AF_INET6是ipv6),src是传入的字符串IP地址,dst是转换得到的大端整形IP存入的内存块。
       成功返回1,失败返回0或者-1。
     b.const char * inet_ntop(int af, const void * src, char * dst, socklen_t size);
       将整形网络字节序IP地址转换为主机字符串IP地址,af表示地址族(AF_INET是ipv4,AF_INET6是ipv6),src是传入的整形大端IP地址,dst是转换得到的字符串IP地址,size是dst最多存储多少个字节。
       成功返回dst对应的地址,失败返回NULL。
  (4)、将字符串类型的IP地址进行大端转换:[只IPv4]
     a.in_addr_t inet_addr(const char * cp);
       点分十进制IP转为大端整形
     b.char * inet_ntoa(struct in_addr in);  
       大端整形转为点分十进制IP

三、TCP通信流程
1、TCP和UDP区别:
  (1)、TCP: 
      a.面向连接的,连接需要3次握手,断连需要4次挥手,即双向连接、双向断开。
      b.流式传输协议,发送端和接收端处理数据量可以不均等,比如发送端一次发送10M数据,接收端每次接收1M数据,分10次接收。
      c.数据可靠的,有数据校验机制,若数据包丢失则自动重传。
  (2)、UDP: 
      a.面向无连接的,双方直接通信,无需连接。
      b.报文式传输协议,发送端和接收端处理数据量均等,比如发送端一次发送1M报文,接收端要么一次接收1M报文,要么丢包,不存在接收一半报文的情况。
      c.数据不可靠的,报文丢失就丢了。

2、TCP通信流程:
     

      TCP客户端                     TCP服务器端

                                    socket()
                                       |
                                     bind()
       socket()                        |
         |                          listen()
       connect()                       |
         |    \       建立连接       accept()
         |        \  \  \  \  \        |
         |                        阻塞直到有客户端连接
         |           请求数据           |
        send()------------------>     recv()
         |           应答数据           |
        recv()<------------------     send()
         |           结束连接           |
         |          / / / / / / / /  recv()
         |        /                    |
        close()                      close()

3、Linux下TCP通信API:
  (1)、int socket(int domain, int type, int protocol);
      a.创建监听套接字。
      b.domain协议族,AF_INET是IPv4协议,AF_INET6是IPv6协议,AF_UNIX是本地进程间通信等。
      c.type套接字类型,SOCK_STREAM是面向连接的流式套接字(TCP),SOCK_DGRAM是无连接数据报套接字(UDP),SOCK_RAW是原始套接字(直接访问网络层协议)。
      d.protocol是传输协议,通常设为0,系统会根据前两个参数自动选择。
      e.成功返回非负整数(socket文件描述符),失败返回-1。
  (2)、int setsockopt(int sockfd, int level, int optname, const void * optval, socklen_t optlen);
      a.设置套接字的底层行为,如地址复用、缓冲区大小、超时控制等。
      b.sockfd是由socket()创建的套接字描述符。
      c.level是选项协议层(SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP)。
      d.optname是选项名称,SO_REUSEADDR允许地址重用,SO_RCVBUF设置接收缓冲区大小,......。
      e.optval指向选项值的指针。
      f.optlen选项值的长度。
      g.成功返回0,失败返回-1。
  (3)、int bind(int sockfd, const struct sockaddr * addr, socklen_t addrlen);
      a.绑定地址与端口。
      b.sockfd是由socket()创建的套接字描述符(用于监听)。
      c.addr是包含协议地址信息的结构体指针(需sockaddr_in或sockaddr_in6,包含的IP和端口必须是大端的网络字节序)。
      d.addrlen是地址结构体的长度。
      e.成功返回0,失败返回-1。
  (4)、int listen(int sockfd, int backlog);
      a.设置监听,成功后将监听客户端的连接。
      b.sockfd是由socket()创建的套接字描述符。
      c.backlog理解: 内核为sockfd维护两个队列SYN队列(收到SYN但未完成三次握手的半连接请求)和Accept队列(存储已建立的全连接),min(backlog, 128)决定Accept队列最大长度。
        若backlog = 1,仅允许1个连接在Accept队列中,后续请求会被直接拒绝,若此时accept()取走队列中的连接后,新连接可正常进入Accept队列。
      d.成功返回0,失败返回-1。
      e.注意: backlog并不代表只能与backlog个客户端通信,而是代表在Accept队列中,最多存在min(backlog, 128)个连接,若被accept()取走连接后,后续新连接会继续存放在Accept队列中。
  (5)、int accept(int sockfd, struct sockaddr * addr, socklen_t * addrlen);
      a.从监听套接字的已完成连接队列(Accept队列)中取出下一个已建立的连接,返回一个新的套接字描述符用于数据通信。
        若队列为空且监听套接字为阻塞模式,进程将休眠等待; 若队列为空且监听套接字为非阻塞模式,立即返回错误。
        原监听套接字继续监听新连接,新套接字专用于与客户端数据通信。
      b.sockfd是由socket()创建的监听套接字描述符,默认是阻塞模式,可通过fcntl()设置为非阻塞模式。
      c.addr用于存储客户端地址信息。
      d.addrlen是addr结构体的长度,调用前需初始化为addr结构体的长度,返回时更新为实际地址长度。
      e.成功时返回一个新的套接字描述符,专门用于与客户端通信;失败返回-1。
  (6)、int connect(int sockfd, const struct sockaddr * addr, socklen_t addrlen);
      a.客户端向服务器发起TCP连接请求,触发三次握手过程(SYN、SYN+ACK、ACK)
      b.sockfd是客户端用于通信的套接字。
      c.addr指向目标服务器的地址结构体(struct sockaddr_in或struct sockaddr_in6)指针,包含IP和端口。
      d.addrlen是addr指向的结构体长度。
      e.成功返回0,失败返回-1。
      f.默认阻塞模式,调用后线程挂起,直到三次握手成功或失败。非阻塞模式调用后立即返回-1,并设errno=EINPROGRESS,需通过select()/poll()检查连接状态。
  (7)、ssize_t recv(int sockfd, void * buf, size_t len, int flags);
      a.在TCP通信中用于接收数据,将数据从内核接收缓冲区复制到用户指定的缓冲区中。
      b.sockfd是已连接的套接字描述符。
      c.buf是用户提供的接收缓冲区。
      d.len是缓冲区长度。
      e.flags控制接收行为,通常置0。
      f.返回值,>0表示成功接收的字节数(=len可能有未读完数据),=0表示对端正常关闭连接,=-1表示错误发生(EAGAIN/EWOULDBLOCK非阻塞模式下无数据可读,ECONNRESET连接被强制重置)
      g.和read()函数类似,只多出flags参数。
  (8)、ssize_t send(int sockfd, const void * buf, size_t len, int flags);
      a.用于在已建立的TCP连接上发送数据。
      b.sockfd是已连接的通信套接字描述符。
      c.buf是待发送数据的缓冲区地址。
      d.len是数据长度(字节数)。
      e.flags是控制标志。
      f.若len超过缓冲区总大小则返回-1;若缓冲区剩余空间不足,阻塞模式下等待空间释放,非阻塞模式下直接返回EAGAIN/WSAEWOULDBLOCK;将buf中数据拷贝至内核发送缓冲区,不立即发送,由TCP协议异步处理传输。
      g.成功返回实际拷贝的字节数,失败返回-1。
      h.和write()函数类似,只多出flags参数。
  (9)、int close(int sockfd);
      a.释放描述符、缓冲区,并减少内核引用计数,若为最后一个引用,则完全关闭连接(内核尝试发送缓冲区剩余数据,并触发四次挥手)。
      b.sockfd为套接字描述符(监听套接字、通信套接字)。
      c.成功返回0,失败返回-1。
  
4、sockaddr数据结构:
  (1)、通用结构:

      struct sockaddr
      {
          sa_family_t sa_family;  //地址族(如AF_INET、AF_INET6、AF_UNIX)
          char sa_data[14];       //协议特定地址数据(IP+端口等),其具体格式由sa_family决定。
      };


  (2)、专用结构体sockaddr_in(IPv4专用)

      struct sockaddr_in
      {
          sa_family_t    sin_family;  //必须为AF_INET
          in_port_t      sin_port;    //16位端口号(网络字节序,用htons()转)
          struct in_addr sin_addr;    //32位IPv4地址(网络字节序,用inet_addr()或inet_pton()转)
          char           sin_zero[8]; //填充字段(保持与sockaddr大小一致)
      };


  (3)、专用结构体sockaddr_in6(IPv6专用)

      struct sockaddr_in6 
      {
          sa_family_t     sin6_family;    //必须为AF_INET6   
          in_port_t       sin6_port;      //16位端口号(网络字节序,用htons()转)  
          uint32_t        sin6_flowinfo;  //32位IPv6流标签(通常设为0)   
          struct in6_addr sin6_addr;      //128位IPv6地址(网络字节序,用inet_pton()转) 
          uint32_t        sin6_scope_id;  //32位接口索引(用于链路本地地址)  
      };

   (4)、注意:
     sockaddr_in6结构体是28字节,sockaddr和sockaddr_in结构体是16字节。
     所以使用时,程序员必须先定义sockaddr_in6或sockaddr_in结构体对象并赋值,然后将对象地址强转成sockaddr *传入bind()、connect()、accept()等函数。
     bind()、connect()、accept()函数内部会通过sa_family区分实际类型并转成具体的结构体。

5、套接字文件描述符:
  (1)、套接字文件描述符,分为两类:
      a.监听新的客户端连接。
      b.数据通信。
  (2)、在服务器端,一般只有一个用于监听的套接字文件描述符,而有N个用于数据通信的套接字文件描述符。
       在客户端,一般只有用于数据通信的套接字文件描述符。
  (3)、一个套接字文件描述符对应内核中两块内存:
      a.读缓冲区,用于接收数据。
      b.写缓冲区,用于发送数据。
  (4)、read()/recv()是从对应内核的读缓冲区中拷贝数据到用户设定的缓冲区中。
       write()/send()是从用户设定的缓冲区中拷贝数据到对应内核的写缓冲区中。
       这些函数并不是直接读/写网络中的数据,而内核缓冲区中的数据是由内核维护。

 (5)、通信流程中有些和读/写缓冲区相关的API,就可能会涉及到阻塞:
      a.比如accept()函数,是从监听套接字对应的读缓冲区获取连接,如果当前读缓冲区没有连接且套接字是阻塞模式,accept()函数就会挂起等待,直到有新的连接。
      b.比如connect()函数,是客户端套接字向对应写缓冲区发送连接请求,如果当前套接字是阻塞模式,则connect()会挂起等待,直到握手成功、失败或超时(默认75秒)。
      c.比如read()/recv()函数,是从通信套接字对应的读缓冲区获取数据,如果当前读缓冲区没有数据且套接字是阻塞模式,read()/recv()就会挂起等待,直到有新的数据。
      d.比如write()/send()函数,是向通信套接字对应的写缓冲区写入数据,如果当前写缓冲区满了且套接字是阻塞模式,write()/send()就会挂起等待,直到缓冲区有空间可用。

6、设置非阻塞模式API:
  (1)、int ioctlsocket(SOCKET s, long cmd, u_long * argp);
      a.Windows平台下用于控制套接字I/O模式,主要用于设置非阻塞模式。
      b.s是目标套接字描述符。
      c.cmd是控制命令(如FIONBIO设置非阻塞模式)。
      d.argp指向u_long类型变量,1启用非阻塞,0恢复阻塞。
      e.成功返回0,失败返回-1。

  (2)、int fcntl(int fd, int cmd, ...);
      a.Unix/Linux平台下用于控制文件描述符行为,主要用于设置套接字非阻塞模式。
      b.fd是目标文件描述符。
      c.cmd是控制命令(如F_GETFL获取标志(阻塞/非阻塞模式)、F_SETFL设置标志(0_NONBLOCK非阻塞模式))。
      d.arg可选参数,类型取决于cmd。
      e.成功返回值根据cmd来定,失败返回-1。
      f.例如设置套接字非阻塞模式:    

       int flags = fcntl(serverSocket, F_GETFL, 0);
       fcntl(serverSocket, F_SETFL, flags | O_NONBLOCK);


网站公告

今日签到

点亮在社区的每一天
去签到