当前位置: 首页 > news >正文

c++之网络编程

网络编程:使得计算机程序能够在网络中发送和接受数据,从而实现分布式系统和网络服务的功能。

作用:使应用程序能够通过网络协议与其他计算机程序进行数据交换

基本概念

套接字(socket):

       套接字是网络通信的端点(端点指通信的两个参与者中的每一个)。允许不同进程之间或者同一进程不同线程之间通过网络交换数据。它是应用程序与网络之间的接口(是应用层与TCP/IP协议族通信的中间软件抽象层)。套接字允许应用程序使用网络协议(TCP/UDP)进行数据传输。

                   

套接字描述符:

套接字描述符是一个由操作系统分配的整数,用于唯一标识一个打开的套接字。通过套接字描述符,程序可以对套接字进行读写操作、设置属性、管理连接等。通过socket()函数创建一个新的套接字,就会返回一个int类型的整数,这个整数就是套接字描述符。可以使用它进行连接、发送、接收数据等。实例connect(套接字描述符)  close(套接字描述符)。套接字描述符的生命周期是在创建套接字时由系统分配,并在套接字关闭时释放,关闭套接字后,描述符可以被重新分配给其他套接字。系统为每个运行的进程维护一张单独的文件描述符表,当创建一个socket时,就将其对应的文件描述符写入到上述的文件描述符表中,操作系统把该描述符作为索引访问进程描述符表,通过指针找到保存该套接字文件所有信息的数据结构。

                        

创建套接字后,套接字内部包含很多字段,但是系统创建套接字后,大多数字段没有填写。应用程序在创建完套接字后,还需要调用其他过程来填充这些字段

                         

协议(protocol):

网络协议是用于数据交换的规则和标准。例如,TCP(传输控制协议)和UDP(用户数据报协议)是两个常用的协议。TCP是面向连接的,保证数据可靠性,而UDP是无连接的,速度快但并不保证可靠性。

端口(port):

端口是用来标识特定进程或服务的逻辑地址。例如,HTTP协议通常使用端口80,FEP协议使用端口21

IP地址(IP Address):

是网络中每台计算机的唯一标识。它用于定位网络中的设备并进行数据传输

客户端-服务器模型(client-server model)

在这个模型中,客户端发起请求,服务器提供响应。客户端和服务器之间通过网络进行通信

SYN

用于建立连接

FIN

用于请求终止连接

ACK

用于确认收到的请求连接/确认关闭连接请求

序列号(sequence number)

用于数据包排序

确认号(acknowledgement number)

用于确认收到的数据包

TCP和UDP的详细介绍

TCP传输控制协议

特点

  1. 面向连接:在数据传输之前,TCP必须先建立连接。客户端和服务器通过三    次握手过程来建立连接。
  2. 可靠性:TCP提供可靠的数据传输。它通过序列号、确认应答、重传机制来确保数据的完整性和顺序
  3. 数据流控制:TCP使用滑动窗口来控制数据流量,防止接收端因处理能力不足而溢出
  4. 拥塞控制:TCP会动态调整发送速度,以避免网络拥塞。常用的算法包括慢启动、拥塞避免、快重传和快恢复
  5. 数据包顺序:数据包在TCP中按发送顺序到达接收端。TCP为每个数据包分配序列号,并对数据包进行排序
  6. 错误检测和纠正:TCP包含校验和字段,用于检测数据包在传输过程中是否被损坏。如果检测到错误,TCP会重新传输

优点

  1. 可靠性高:数据传输过程中发生的数据丢失或错误会被重传,确保数据的完整性和正确性。
    1. 提供顺序保证:数据包按顺序到达接收端,适合需要数据顺序的应用场景,如文件传输、网页浏览等。
  2. 流量控制:防止网络拥塞,提供稳定的传输性能。
  3. 拥塞控制:动态调整数据发送速率,减少网络拥塞。

缺点

  1.  开销较大:由于需要建立连接、维护状态、处理错误和重传机制,TCP协议的开销较大。

    延迟较高:建立连接和数据传输过程中进行的可靠性检查和流量控制增加了延迟,不适合实时性要求高的应用。

  2. TCP三次握手和四次挥手

三次握手

TCP协议建立连接的过程。这个过程确保双方都能准备好进行数据传输。

过程:

1.SYN:客户端发送连接请求

        客户端向服务器发送一个带有SYN(同步)标志的数据包,表示请求建立连接。

        标志位:SYN=1

        序列号:客户端选择一个初始序列号(ISN:这个序列号是客户端用于标识其发送的数据

        的起始编号)在数据包中发送。

        客户端 → 服务器 : SYN, Seq = x(ISN)

2.SYN-ACK:服务器确认请求

        服务器接收到客户端的SYN数据包后,发送一个带有SYN和ACK(确认)标志的数据包,确认收到连接请求。

        标志位:SYN=1,ACK=1

        确认号:服务器确认客户端的序列号(x+1),这里的x+1代表的意思是确认  收到客户端的

        SYN数据包(序列号为x),并期望接收下一个客户端  发来的序列号是x+1。并选择自己的初

        始序列号y。

        服务器 → 客户端 : SYN, ACK, Seq = y, Ack = x + 1

3.ACK:客户端确认连接建立

        客户端收到服务器的SYN-ACK数据包后,发送一个带有ACK标志的数据包,确认服务器的序列号(y)和自己的序列号(x+1)。

        标志位:ACK=1

        确认号:客户端确认服务器的序列号(y+1),这里的y+1代表的意思是确认  收到服务端的

        SYN数据包(序列号为y),并期待接收下一个服务器  发来的序列号是y+1。

        客户端 → 服务器 : ACK, Seq = x + 1, Ack = y + 1

经过三次握手后:连接建立成功,客户端和服务器都进入已连接的状态,可以开始传输数据。

                     

        客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SUN J包,调用accept函数接收请求向客户端发送SYN K,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SUN K,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕。

上述三次握手过程我们可以通过网络抓包来查看具体流程:

例如:服务器开启了9502端口。使用tcpdump来抓包

Tcpdump -iany tcp port 9502
使用telnet连接 
telnet 127.0.0.1 9502

示例:

14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378, win 32792, options [mss 16396,sackOK,TS val 255474104 ecr 0,nop,wscale 3], length 0(1)
14:12:45.104701 IP localhost.9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379, win 32768, options [mss 16396,sackOK,TS val 255474104 ecr 255474104,nop,wscale 3], length 0(2)
14:12:45.104711 IP localhost.39870 > localhost.9502: Flags [.], ack 1, win 4099, options [nop,nop,TS val 255474104 ecr 255474104], length 0(3)

其中: [S] 表示这是一个SYN请求

            [S.] 表示这是一个SYN+ACK确认包:

            [.] 表示这是一个ACT确认包,

            (client)SYN->(server)SYN->(client)ACT 就是3次握手过程

(1)104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378
客户端IP localhost.39870 (客户端的端口一般是自动分配的) 向服务器localhost.9502发送syn包(syn=j)到服务器》
syn包(syn=j) : syn的seq= 2927179378  (j=2927179378)
(2)14:12:45.104701 IP localhost.9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379,
收到请求并确认:服务器收到syn包,并必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包:
此时服务器主机自己的SYN:seq:y= syn seq 1721825043。
ACK为j+1 =(ack=j+1)=ack 2927179379 
(3)14:12:45.104711 IP localhost.39870 > localhost.9502: Flags [.], ack 1,
客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1)
客户端和服务器进入ESTABLISHED状态后,可以进行通信数据交互。此时和accept接口没有关系,即使没有accepte,也进行3次握手完成。

三次握手后建立连接后的数据传输过程

接上:

14:13:01.415407 IP localhost.39870 > localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], length 7
14:13:01.415432 IP localhost.9502 > localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0
14:13:01.415747 IP localhost.9502 > localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18
14:13:01.415757 IP localhost.39870 > localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0

其中:[P.]表示这是一个数据推送,可以是从服务器向客户端推送,也可以是从客服端向服务器推送

        [.] 表示这是一个ACT确认包

        Win 4099是指滑动窗口的大小

        Length 18指数据包的大小

四次挥手

TCP协议用来终止连接的过程。这个过程确保双方都能正常关闭连接,避免数据丢失。

过程:

1.FIN:客户端请求终止连接

        客户端向服务器发送一个带有FIN(结束)标志的数据包,表示请求关闭连接。

        标志位:FIN=1

        序列号:客户端发送的序列号u

        客户端 → 服务器 : FIN, Seq = u

2.ACK服务器确认关闭连接请求

        服务器收到客户端的FIN数据包后,发送一个带有ACK标志的数据包,确认收到关闭通知。

        标志位:ACK=1

        确认号:服务器确认客户端的序列号(u+1)

        服务器 → 客户端 : ACK, Seq = v, Ack = u + 1

3.FIN:服务器请求关闭连接

        服务器在完成数据传输后,向客户端发送一个带有FIN标志的数据包,表示请求关闭连接。

        标志位:FIN=1

        序列号:服务器发送的序列号v+1

        服务器 → 客户端 : FIN, Seq = v + 1

4.客户端确认关闭的连接请求

        客户端收到服务器的FIN数据包后,发送一个带有ACK标志的数据包,确认收到关闭请求。

        标志位:ACK=1

        确认号:客户端确认服务器的序列号v+1(按照正常逻辑来说确认好应该是  v+2,但由于这已

        经是最后一步应答,不会再有更多的数据包发送, 因此这里写v+1即可,即只需指定接收到

        的seq,而不指定接下来期 望接收的seq)。

        客户端 → 服务器 : ACK, Seq = u + 1, Ack = v + 1

经过四次挥手后,连接成功终止,客户端和服务器都进入关闭状态。客户端和服务器可能会经历一个TIME_WAIT状态,确保所有的数据都能被正确处理。

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

(1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送(报文段4)。

(2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。

(3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A(报文段6)。

(4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。

                

1.某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;

2.另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;

3.一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;

4.接收到这个FIN的源发送端TCP对它进行确认。

这样每个方向上都有一个FIN和ACK。

注意:[F] 表示这是一个FIN包,是关闭连接操作,client/server都有可能发起

为什么建立连接协议是三次握手,而关闭连接确实四次握手呢?

        这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建立连接请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当接收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所哟你可以未必马上关上socket,也即你可能还需要发送一些数据给对方后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以断开连接请求的ACK报文和FIN报文多数情况下都是分开发送的。

为什么TIME_WAIT状态还需要等待2MSL后才能返回到CLOSE状态?

        这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。

  1. UDP用户数据报协议

特点

  1. 无连接:UDP是无连接的,发送数据之前无需建立连接。每个数据报独立处理。

    不可靠:UDP不提供数据传输的可靠性保证。数据包可能丢失、重复或乱序,UDP不会进行重传或顺序保证。

    没有流量控制、拥塞控制:发送速度不受限,接收端可能会遭遇数据溢出不会调整发送速率,可能导致网络阻塞的情况 头部开销小:头部信息较少,占用带宽少。

优点

低延迟:由于无连接和缺乏可靠性机制,UDP提供较低的传输延迟

开销小:头部开销小,适合对传输速率要求高的应用

简单:无需建立连接和维护连接,易于实现

支持广播和组播:UDP支持将数据包发送到多个接受者,适合需要广播或组播的应用场景。

缺点

不可靠:不能保证数据的到达、完整性和顺序,适合对可靠性要求不高的场景。

没有流量控制和拥塞控制:可能导致数据丢失或网络拥塞,尤其是在高流量环境下。

TCP/IP(传输控制协议/互联网协议)

        是现代计算机通信的基础,是一组协议的集合,用于在计算机中进行数据传输和通信。TCP/IP分为多个层次,每一个层次负责不同的通信功能。

分层(4层或5层):

1.应用层(application layer)

为应用程序提供网络服务的接口,处理特定的应用协议。

协议:http用于网页传输、FTP用于文件传输、SMTP用于电子邮件传输、DNS用于域名解析。

2.传输层(transport layers)

提供主机之间的数据传输服务,确保数据完整性和可靠性。

协议:TCP,UDP

示例:应用程序通过TCP/UDP进行数据传输

3.网络层(network layer)

负责数据包的路由选择和转发,确保数据从源主机到达目标主机

协议:IPV4/IPV6

4.链路层(link layer)

处理物理网络连接和数据帧的传输,负责在局部网络中传输数据。

协议:Ethernet(用于局域网的标准协议)、wifi(用于无线网的标准协议)

5.物理层(physical layer)(可选)

处理物理媒介的传输,例如电缆、光纤、或无线信号。

Socket在TCP/IP中的位置与作用

位于应用层与传输层/网络层之间,是应用层与TCP/IP协议族通信的中间软件抽象层。

             

TCP客户端与TCP服务器端通信的大致流程

        服务器端先初始化一个socket,然后端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个socket,然后连接服务器connect(),如果连接成功,这是客户端与服务器的连接就建立成功了。客户端发送数据请求,服务器接受请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

                    

网络编程的关键步骤

创建套接字(socket creation)

套接字是网络通信的基础。创建套接字时,需要指定协议族(如IPv4或IPv6)、套接字类型(如流式套接字或数据报套接字)以及协议(如TCP或UDP)

绑定(binding)

绑定将套接字与本地地址(IP地址和端口)关联起来。这使得套接字能够在指定的端口上接收数据。

监听(listening)

监听是服务端准备接受客户端连接的过程。服务器通过监听特定的端口来等待客户端的连接请求。

接受连接(accepting connections)

在监听状态下,服务器接受来自客户端的连接请求。一旦连接建立,服务器和客户端可以进行数据交换

发送和接收数据(sending and receiving data)

一旦建立连接,数据可以在客户端和服务器之间传输。这可以是请求和响应,或任何其他数据。

关闭套接字(closing socket)

数据传输完成后,套接字需要关闭,以释放资源。

基本的socket函数

socket()

int socket(int protofamily,int type,int protocol);

返回sockfd套接字描述符,这个描述符唯一标识一个socket,会用这个sockfd进行后续的操作。

参数:

①protofamily协议域/族:决定了socket的地址类型,在通信中必须采用对应的地址。

AF_INET:要用ipv4地址(32位)与端口号(16位的)组合
AF_INET6:要用ipv6地址(128位)与端口号(16位的)组合
AF_UNIX/AF_LOCAL:要用一个绝对路径名作为地址

②type:指定socket类型

SOCKET_STREAM:字节流套接字,适用于TCP
SOCKET_DGRAM:数据报套接字,适用于UDP

③protocol:指定协议

IPPROTO_TCP:TCP传输协议
IPPROTO_UDP
IPPROTO_SCTP
IPPROTO_TIPC

bind()

int bind(int sockfd,const struct *addr,socklen_t addrlen);

        将一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。

        通常服务器在启动的时候都会绑定一个众所周知的地址(如服务器+端口号),用于提供服务,客户就可以通过它来连接服务器;而客户端就不用指定,他会有一个系统自动分配的端口和ip的地址组合。这就是为什么通常服务器在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

参数:

①sockfd:socket描述符

②addr:一个const struct sockaddr*指针,指向要绑定给sockfd的协议地址。这个根据socket()函数中的参数protofamily不同而不同

对于AF_INET(ipv4)来说结构体定义为:

对于AF_INET6(ipv6)来说结构体定义为:

③addrlen:对应的是地址的长度

补充:字节序

网络字节序:TCP/IP中二进制整数在网络中传输时的次序

以四个字节的32bit值传输次序为例:首先是0-7bit,其次是8-15bit,然后16-23bit,最后是24-31bit,即多字节的数据按照从高位到低位的顺序发送。这种称作大端字节序。所以在将一个地址绑定到socket的时候,先将主机字节序转换为网络字节序(即大端字节序)

字节序:计算机系统内部如何存储多字节数据(如整数)的字节顺序

  1. 大端字节序Big-Endian:高位字节存放在内存的低地址端,低位字节排放在内存 的高地址端。整数 0x12345678 会以 12 34 56 78 的顺 序存储在内存中。
  2. 小端字节序Little-Endian:高位字节存储在内存的高位地址,低位字节存储在内   存的低位地址。0x12345678 会以 78 56 34 12 的顺序   存储在内存中。

转换函数:

在实际编程中,通常会使用系统提供的函数来进行字节序的转换

htonl() 和 htons():将主机字节序转换为网络字节序(htonl 是 "host to network long" 的缩写,htons 是 "host to network short" 的缩写)。
ntohl() 和 ntohs():将网络字节序转换为主机字节序(ntohl 是 "network to host long" 的缩写,ntohs 是 "network to host short" 的缩写)。

inet_pton/inet_ntop    #include <arpa/inet.h>  

支持ipv4和ipv6地址格式,这两个函数确保数据在网络上传时能正确解释和处理

inet_pton:将文本格式的IP地址转换为网格格式的二进制地址

int inet_pton(int af, const char *src, void *dst);

成功返回1,失败返回0/-1,并设置erron

参数:

af:地址族,AF_INET/AF_INET6

src:指向一个c字符串的指针,该字符串表示IP地址(文本格式)

dst:指向一个内存区域的指针,用于存储转换后的网络格式地址。对于ipv4 指向一个struct in_addr结构体,对于ipv6指向一个struct in6_addr结构体。

inet_ntop:将网格格式的二进制地址转换为文本格式的IP地址

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

成功时返回dst指针,失败时返回nullptr,并设置errno

参数:

af:地址族,AF_INET/AF_INET6

src:指向一个内存区域的指针,用于存储转换后的网络格式地址。对于ipv4 指向一个struct in_addr结构体,对于ipv6指向一个struct in6_addr结构体。

dst:指向一个字符数组的指针,用于存储转换后的文本格式地址

size:dst数组的大小,以字节为单位

listen()  Connect()

        作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket(服务器端的socket),如果客户端这时调用connect()发出连接请求,服务器就会接收到这个请求。

int listen(int sockfd,int backlog);

参数:

①sockfd:要监听的socket描述字(服务器端的)

②backlog:相应的socket可以排队的最大连接个数

注意:socket()创建的socket默认是主动类型的,listen()将socket变为被动      类型,等待客户端的连接请求。

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

参数:

①sockfd:客户端socket描述字

②addr:服务器的socket地址

③addrlen:socket地址的长度

注意:客户端通过调用connect函数来建立与TCP服务器的连接。

accept()函数

        TCP服务器依次调用socket()、bind()、listen()之后就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就可以向TCP服务器发送一个连接请求,TCP服务器监听到这个请求后就会调用accept()函数接受请求,如果accept成功返回,这样连接就建立好了。之后可以通过对accept返回的套接字(connect_fd)的操作来完成与客户的网络I/O操作(类似于普通文件的读写I/O操作)。

int accept(int sockfd , struct sockaddr *addr , socklen_t *addrlen);

会返回一个连接的描述符,connect_fd

参数:

①sockfd:这是一个用于监听的套接字描述符,即通过socket()、bind()、 listen()函数创建并设置的服务器端套接字,accept函数将 检查这个套接字是否有来自于客户端的连接请求。如果有他将接受这个连接请求,并返回一个新的套接字描述符 (connect_fd),用于与客户端进行数据通信。

②addr:用于存储客户端的地址信息。这个地址描述了连接到服务器   的客户端的网络地址(ip和端口号).当accept()成功返回时,addr指向的内存区域将被填充为客户端的地址信息(最开始   传入的是一个空的)。若客户端的地址信息不重要,可将这个参数设置为NULL。

③addlen:指定addr指向的地址的结构体的大小。如果不需要获取客户端地址信息,可以将这个参数设置为NULL 。

注意:accept默认会阻塞进程,直到有一个客户建立连接后返回,它返回的是

个新可用的套接字,这个套接字是连接套接字connect_fd。

区分两种套接字:

  1. 监听套接字:如accept的参数sockfd他是监听套接字,是服务器调用listen()之后生成的,由一个主动连接的套接字变身为一个监听套接字。
  2. 连接套接字:accept()函数返回的已连接的socket描述字(一个连接套接字), 他代表着一个网络已经存在的点点连接。

注意:服务器只会创建唯一一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接都创建了一个已连接的socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket 描述字就会被关闭。即监听套接字在一个服务器的生命周期中只能有一个,而连接套接字可以有多个。连接套接字没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd一样的端口号。

IO操作

在上述4步骤后,服务器端与客户端已经建立连接。可以用网络I/O进行读写操作了,即实现网络中不同进程与线程之间的通信。

网络I/O操作有下面常见的几组函数:

read()/write()

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

        read函数是负责从fd中读取内容。当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。

ssize_t write(int fd, const void *buf, size_t count);

        write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有两种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

recv()/send()

#include <sys/types.h>

#include <sys/socket.h>

发送数据

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数:

Sockfd:套接字文件描述符

Buf:指向要发送的数据的缓冲区

Len:要发送的数据的字节数

Flags:通常设置为0,但可以指定其他标志,如MSG_OOB等

发送成功后返回发送的字节数;出错时返回-1,并设置‘errno’以指示错误原因。

注意:#include <cerrno> 引入头文件后就能使用’errno’,它是一个全局变量,   它在出错时被系统调用和库函数设置,可以通过#include <cstring>    strerror(errno)来将erron转为字符串,方便打印。

errno有以下几种

①EINVAL:参数无效

②ENGAIN/EWOULDBLOCK:资源暂时不可用(通产在非阻塞模式下)

③ECONNRESET:连接被对方重置

④ENOTCONN:套接字未连接(操作未连接的套接字)

接收数据

  ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数说明:

sockfd:套接字文件描述符,用于标识要接收数据的套接字。

buf:指向存放接收数据的缓冲区的指针。

len:指定缓冲区的大小(以字节为单位),即本次 recv 调用最多接收的字节数。

flags:通常设为 0,但可以指定其他标志,例如 MSG_OOB(接收带外数据)等。

返回值:

成功时,返回实际接收到的字节数。

连接已关闭时,返回 0,表示对方已正常关闭连接。

失败时,返回 -1,并设置 errno 以指示具体的错误原因。

recvmsg()/sendmsg()

从套接字发送数据

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

从套接字接收数据

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

参数:

Socket:套接字的文件描述符

Msg:指向struct msghdr结构的指针,该结构定义了要发送或接受的数据。

struct msghdr {void         *msg_name;       // 消息的目标地址socklen_t     msg_namelen;    // 地址长度struct iovec *  msg_iov;        // 指向数据缓冲区的指针size_t        msg_iovlen;     // 缓冲区数组中的元素数量void         *msg_control;  // 控制数据的指针(例如,用于传输文件描述符)size_t        msg_controllen; // 控制数据的长度int           msg_flags;      // 消息标志
};

Flags:操作的标志位,通常为0或其他标志位(如MSG_DONTWAIT)。

recvfrom()/sendto()

 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

参数说明:

sockfd:套接字文件描述符,用于标识发送数据的套接字。

buf:指向要发送数据的缓冲区的指针。

len:要发送的数据的字节数。

flags:通常设为 0,但可以指定其他标志,例如 MSG_CONFIRM(用于UDP防止ARP欺骗)等。

dest_addr:指向 sockaddr 结构体的指针,表示目标地址(IP 和端口)。

addrlen:dest_addr 结构体的大小,以字节为单位。

返回值:

成功时,返回实际发送的字节数。

失败时,返回 -1,并设置 errno 以指示具体的错误原因

 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);

参数说明:

sockfd:套接字文件描述符,用于标识接收数据的套接字。

buf:指向存放接收数据的缓冲区的指针。

len:指定缓冲区的大小(以字节为单位),即本次 recvfrom 调用最多接收的字节数。

flags:通常设为 0,但可以指定其他标志,例如 MSG_WAITALL(等待完整数据)。

src_addr:指向 sockaddr 结构体的指针,存储发送方的地址信息(IP 和端口)。如果不关心发送方地址,可以传 NULL。

addrlen:指向 socklen_t 类型的指针,表示 src_addr 结构体的大小,调用后会被填充为实际地址长度。如果 src_addr 传 NULL,则 addrlen 也可以传 NULL。

返回值:

成功时,返回实际接收到的字节数。

连接已关闭时,返回 0,表示对方已正常关闭连接。

失败时,返回 -1,并设置 errno 以指示具体的错误原因。

close()

        在服务器与客户端建立连接之后,会进行一些写操作,完成了读写操作就要关闭相应的socket描述字。

int close(int fd);

①fd为某一connect后的已连接套接字符时:此时会终止与该特定用户的连接。这个操作将使得服务器不再与这个客户端进行数据交换。客户端会收到一个连接终止的信号,从而知道连接已经被关闭。此时仍然接受新的客户端建立连接。

②fd为监听套接字符时:此时会停止服务器新的连接请求。对于已经建立的连接,服务器可以继续与这些已经连接的客户端进行数据交换,直到这些套接字被关闭(客户端调用了close)

网络编程入门实例

服务器端:一直监听本机的8000号端口,如果收到连接请求,将接收请求并接收客户端发来的消息,并向客户端返回消息

#include <iostream>
#include <cstring>   // memset、strerror
#include <cstdlib>   // std::exit
#include <cerrno>    // errno
#include <sys/types.h>  // ssize_t
#include <sys/socket.h> // socket
#include <netinet/in.h> // sockaddr_in结构体
#include <unistd.h>     // POSIX系统调用,fork、close
#include <fcntl.h>      // 文件控制选项#define DEFAULT_PORT 8000  // 默认端口号
#define MAXLINE 4096       // 接收缓冲区的最大字节数int main(int argc, char** argv) {int socket_fd, connect_fd;                      // 套接字文件描述符struct sockaddr_in servaddr;                    // 服务器地址结构体char buff[MAXLINE];                             // 用于存储接收到的数据 ssize_t n;                                       // 接收到的字节数// 初始化套接字socket_fd = socket(AF_INET, SOCK_STREAM, 0);     // IPV4的流式套接字if (socket_fd == -1) {std::cerr << "创建套接字失败: " << std::strerror(errno) << " 错误码 " << errno << std::endl;std::exit(EXIT_FAILURE);}// 初始化服务器地址结构std::memset(&servaddr, 0, sizeof(servaddr));     // 将服务器地址结构清零servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);    // IP地址设置为INADDR_ANY,让系统自动选择本机IP地址servaddr.sin_port = htons(DEFAULT_PORT);         // 设置端口号// 将本地地址绑定到创建的套接字上if (bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {std::cerr << "绑定套接字失败: " << std::strerror(errno) << " 错误码 " << errno << std::endl;std::exit(EXIT_FAILURE);}// 开始监听客户端的连接请求if (listen(socket_fd, 10) == -1) {std::cerr << "监听套接字失败: " << std::strerror(errno) << " 错误码 " << errno << std::endl;std::exit(EXIT_FAILURE);}std::cout << "========等待客户端连接=========" << std::endl;while (true) {// accept之前是阻塞的,直到客户端连接connect_fd = accept(socket_fd, nullptr, nullptr);  // 接收客户端连接if (connect_fd == -1) {std::cerr << "接收客户端连接失败: " << std::strerror(errno) << std::endl;continue;   // 继续等待其他客户端连接}// 连接成功后,接收客户端发送的数据n = recv(connect_fd, buff, MAXLINE, 0);   // 从客户端接收数据if (n < 0) {std::cerr << "接收数据失败: " << std::strerror(errno) << std::endl;close(connect_fd);   // 关闭与此客户端的连接套接字continue;            // 继续等待其他客户端连接}// 向客户端发送数据if (fork() == 0) {    // 创建子进程来处理客户端请求if (send(connect_fd, "hello connect successful", 26, 0) == -1) {std::cerr << "发送数据失败: " << std::strerror(errno) << std::endl;}close(connect_fd);std::exit(EXIT_SUCCESS);}// 父进程继续处理其他客户连接buff[n] = '\0';   // 添加字符结束符std::cout << "接收到客户消息: " << buff << '\n'; // 打印接收到的消息close(connect_fd);}close(socket_fd);   // 关闭监听套接字return 0;
}

客户端

#include <iostream>      // 标准输入输出
#include <cstring>       // memset, strerror 等函数
#include <cstdlib>       // exit 等函数
#include <cerrno>        // errno 全局错误变量
#include <sys/types.h>   // 数据类型,如 ssize_t
#include <sys/socket.h>  // socket, connect 等函数
#include <netinet/in.h>  // sockaddr_in 结构体
#include <arpa/inet.h>   // inet_pton 函数:将IP地址文本转成网络字节序
#include <unistd.h>      // close 函数#define MAXLINE 4096  // 接收和发送缓冲区的最大字节数int main(int argc, char** argv) {int sockfd;                 // 套接字描述符ssize_t n, rec_len;         // n:发送字节数,rec_len:接收到的字节数char recvline[MAXLINE];     // 接收缓冲区char sendline[MAXLINE];     // 发送缓冲区char buf[MAXLINE];          // 临时缓冲区,接收服务器响应struct sockaddr_in servaddr; // 服务器地址结构体// 检查命令行参数是否为2(程序名 + IP地址)if (argc != 2) {std::cerr << "用法:./client <ipaddress>\n";std::exit(EXIT_FAILURE);}// 创建套接字:AF_INET(IPv4),SOCK_STREAM(TCP)sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {std::cerr << "创建套接字失败: " << std::strerror(errno)<< " (错误码: " << errno << ")\n";std::exit(EXIT_FAILURE);}// 初始化服务器地址结构体std::memset(&servaddr, 0, sizeof(servaddr));servaddr.sin_family = AF_INET;            // 地址族 IPv4servaddr.sin_port = htons(8000);          // 设置端口(网络字节序)// 将IP地址从文本转换为网络字节序if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {std::cerr << "无法解析IP地址: " << std::strerror(errno)<< " (错误码: " << errno << ")\n";std::exit(EXIT_FAILURE);}// 连接服务器if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {std::cerr << "连接失败: " << std::strerror(errno)<< " (错误码: " << errno << ")\n";std::exit(EXIT_FAILURE);}// 从标准输入读取要发送的消息std::cout << "请输入要发送的消息:";std::cin.getline(sendline, MAXLINE);// 发送数据if (send(sockfd, sendline, std::strlen(sendline), 0) < 0) {std::cerr << "发送消息失败: " << std::strerror(errno)<< " (错误码: " << errno << ")\n";std::exit(EXIT_FAILURE);}// 接收服务器返回的数据rec_len = recv(sockfd, buf, MAXLINE, 0);if (rec_len < 0) {std::cerr << "接收数据失败: " << std::strerror(errno)<< " (错误码: " << errno << ")\n";std::exit(EXIT_FAILURE);}// 添加字符串终止符并打印接收到的消息buf[rec_len] = '\0';std::cout << "接收到的消息: " << buf << '\n';// 关闭连接close(sockfd);return 0;
}

相关文章:

  • 立创商城、云汉芯城、亿配芯城均启用DeepSeek AI 大模型赋能电子元器件采购平台
  • 第十四届蓝桥杯刷题——day20
  • [官方IP] AXI Memory Init IP
  • 【音视频】AVIO输入模式
  • UnityEditor - 调用编辑器菜单功能
  • 汽车零配件供应商如何通过EDI与主机厂生产采购流程结合
  • Spark读取Apollo配置
  • 在html中如何创建vue自定义组件(以自定义文件上传组件为例,vue2+elementUI)
  • el-upload 上传逻辑和ui解耦,上传七牛
  • Vue里面elementUi-aside 和el-main不垂直排列
  • ClickHouse 中`MergeTree` 和 `ReplicatedMergeTree`表引擎区别
  • 深入理解机器学习:人工智能的核心驱动力
  • OSPF网络协议
  • 【XR手柄交互】Unity 中使用 InputActions 实现手柄控制详解(基于 OpenXR + Unity新输入系统(Input Actions))
  • Windows环境下常用网络命令使用
  • SIEMENS PLC程序解读 ST 语言 车型识别
  • C++面试复习日记(8)2025.4.25,malloc,free和new,delete的区别
  • HDRnet——双边滤波和仿射变换的摇身一变
  • vite+vue构建的网站项目localhost:5173打不开
  • MYSQL之数据类型
  • 初中女生遭多人侵犯后,家属奔波三年要追责那个“案外”的生物学父亲
  • 特朗普政府称将恢复被终止的外国学生合法身份
  • 文庙印象:一周城市生活
  • 民生访谈|公共数据如何既开放又安全?政务领域如何适度运用人工智能?
  • 中科院新增三名副秘书长
  • 外卖口水战四天,京东美团市值合计蒸发超千亿港元