文章目录
- 1 重点知识
- 2 预备知识
- 2.1 理解源IP地址和目的IP地址
- 2.2 认识端口号
- 2.3 理解 "端口号" 和 "进程ID"
- 2.4 理解源端口号和目的端口号
- 2.5 认识TCP协议
- 2.6 认识UDP协议
- 2.7 网络字节序
- 3 socket编程接口
- 3.1 socket 常见API
- 3.2 sockaddr结构
- 4 简单的UDP网络程序
- 4.1 实现一个简单的英译汉的功能
- 5 地址转换函数
- 5.1 字符串转in_addr的函数:
- 5.2 in_addr转字符串的函数:
- 6 简单的TCP网络程序
- 7 简单的TCP网络程序(多进程版本)
- 8 简单的TCP网络程序(多线程版本)
- 9 TCP协议通讯流程
- 10 TCP 和 UDP 对比
- 11 在一个通信过程中,是使用IP/TCP协议簇的一部分,还是任何通信都使用所有的IP/TCP协议呢?
1 重点知识
- 认识IP地址, 端口号, 网络字节序等网络编程中的基本概念;
- 学习socket api的基本用法;
- 能够实现一个简单的udp客户端/服务器;
- 能够实现一个简单的tcp客户端/服务器(单连接版本, 多进程版本, 多线程版本);
- 理解tcp服务器建立连接, 发送数据, 断开连接的流程;
2 预备知识
2.1 理解源IP地址和目的IP地址
通俗易懂地说,源IP地址和目的IP地址就像我们寄快递时的发件人和收件人地址
。
- 源IP地址(发件人):这就像你在寄快递时写上自己的地址,表示这个快递是从哪里发出的。在网络中,源IP地址就是发送数据包的计算机的IP地址,用来告诉接收方这个数据包是从哪里发送出去的。
- 目的IP地址(收件人):这就像你在寄快递时写上收件人的地址,表示这个快递要送到哪里去。在网络中,目的IP地址就是接收数据包的计算机的IP地址,用来告诉发送方这个数据包应该发送到哪里去。
通过源IP地址和目的IP地址,网络设备能够将数据包正确地路由到目的地,实现信息的传递和交流。这就好像通过发件人和收件人的地址,快递公司能够将快递准确无误地送到收件人手中一样。
思考: 我们光有IP地址就可以完成通信了嘛?
想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上,但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析(数据发给你这台电脑了,电脑上的应用程序那么多,QQ、微信、邮箱等等,究竟是发给哪个程序呢?
)。
解答:光有IP地址并不能完成通信。虽然IP地址能够确定接收数据包的目标位置,但还需要其他标识来区分数据要给哪个程序进行解析
。
在发QQ消息的例子中,即使有了IP地址,我们还需要一个标识来区分这个数据包是给哪个QQ用户和哪个QQ程序。这个标识就是端口号(Port Number)
。端口号用来标识发送和接收数据的特定应用程序
。每个应用程序在发送和接收数据时使用不同的端口号,这样接收方就可以根据端口号来区分不同应用程序的数据包。
所以,要完成通信,除了IP地址,我们还需要端口号等其他标识来区分和识别数据包。
记忆点:数据通过ip协议发送到目的主机后,主机中的不同应用程序都有各自的端口号(应用程序区别标识)
。
2.2 认识端口号
端口号(port)是传输层协议的内容(数据进入主机后,该传输到哪个应用程序)。
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程(每个应用程序都是唯一的进程), 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用(唯一性)。
2.3 理解 “端口号” 和 “进程ID”
我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?
解答:系统中的进程和网络中的端口号虽然都是唯一标识一个实体
,但它们所关注的层面和应用场景是不同的
。
- **
进程(PID)是操作系统层面的概念
,**用于标识和管理在计算机上运行的程序实例。每个进程都有唯一的进程ID,用于标识该进程在操作系统中的唯一性。操作系统使用进程ID来跟踪和管理进程的资源、调度和执行。 - **
端口号则是网络层面的概念,**
用于标识和区分不同应用程序之间的通信。在网络通信中,端口号用于标识发送和接收数据的特定应用程序。每个应用程序在网络通信中都使用一个唯一的端口号,以便正确地路由数据包到目标应用程序。
虽然进程和端口号都是唯一的标识符,但它们在不同的层面上发挥作用。进程关注的是操作系统中的程序执行和管理,而端口号关注的是网络通信中应用程序之间的数据传输和区分。
另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;
2.4 理解源端口号和目的端口号
源端口号和目的端口号是网络通信中非常重要的概念,它们分别表示发送和接收数据包的计算机上的应用程序所使用的端口
。
想象一下,在一个咖啡厅里,有多个顾客在用笔记本电脑上网。每个顾客都通过一个唯一的端口号连接到咖啡厅的WiFi网络。当一个顾客发送数据包时,该数据包会包含源端口号,表示发送该数据包的顾客所使用的端口。而目的端口号则表示接收该数据包的服务所使用的端口。
例如,一个顾客通过端口号1234发送了一个请求给Web服务器上的HTTP服务。这个请求的源端口号就是1234,而目的端口号则是HTTP服务所使用的默认端口80。通过使用源端口号和目的端口号,网络设备能够正确地将数据包路由到目标应用程序,实现信息的传递和交流。
总之,源端口号和目的端口号在网络通信中发挥着重要的作用,它们帮助区分不同的应用程序和服务,实现高效和可靠的网络通信。
2.5 认识TCP协议
TCP(Transmission Control Protocol,传输控制协议)
是一种面向连接的、可靠的、基于字节流的传输层通信协议
。TCP旨在适应支持多网络应用的分层协议层次结构,并依靠更低级别的协议提供可靠的、可能不可靠的数据报服务。
TCP的主要特点包括:
- 面向连接:TCP提供了一种可靠的、面向连接的通信方式,它在通信之前需要先建立连接,并在传输数据后关闭连接。这种连接方式允许TCP提供可靠的服务,例如顺序传输和流量控制。
- 可靠性:TCP通过确认机制、重传机制、流量控制和拥塞控制等机制实现数据的可靠传输。确认机制可以确保接收端正确接收到数据,重传机制可以在数据丢失时重新发送数据,流量控制可以防止数据过多导致接收端处理不过来,拥塞控制可以避免网络拥塞。
- 字节流:TCP将数据看作字节流,而不是独立的报文。这意味着发送端发送的数据按照顺序被接收端接收,但接收端需要自行重新组织数据的顺序。
- 可靠的数据传输:TCP提供了可靠的数据传输服务,它通过确认机制和重传机制确保数据的正确传输,并且可以保证数据的有序性和完整性。
- 流量控制:TCP具有流量控制的功能,可以防止因接收端处理能力不足而导致的数据丢失。当接收端处理数据的能力不足时,TCP会降低发送端的发送速率,以确保数据的可靠传输。
- 拥塞控制:当网络拥塞严重时,TCP可以降低发送端的发送速率,以减少数据在网络的传输量,从而避免网络拥塞进一步加剧。
总的来说,TCP协议在网络通信中起着非常重要的作用,它提供了可靠、有序和高效的数据传输服务。通过TCP协议,可以实现各种网络应用,如网页浏览、电子邮件、文件传输等。
2.6 认识UDP协议
UDP(User Datagram Protocol,用户数据报协议)
是OSI参考模型中的传输层协议
,与TCP协议一样用于处理数据包
,是一种无连接的传输层协议
。
UDP的主要特点包括:
- 无连接:UDP不需要在通信之前建立连接,发送数据时可以直接发送,因此具有简单快速的特点。
- 不可靠:UDP没有确认机制和重传机制,数据在传输过程中可能会丢失、重复或乱序到达。因此,使用UDP协议的应用程序需要自己处理数据的可靠传输。
- 面向数据报:UDP将数据划分为称为“数据报”的小块,每个数据报都有自己的目的地址、源地址、校验和等信息。
- 不可知的数据传输:UDP没有流量控制和拥塞控制机制,因此无法控制数据的传输速率。这可能会导致网络拥塞的情况发生。
- 头部开销小:相对于TCP协议,UDP协议头部开销较小,只有8个字节。
总的来说,UDP协议在网络通信中起着重要的作用,它适用于一些不需要可靠传输的应用场景,如流媒体、实时游戏等。虽然UDP协议自身不可靠,但可以通过其他机制实现可靠传输。
2.7 网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址
从低到高
的顺序发出; - 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,
网络数据流
的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
。 - TCP/IP协议规定,网络数据流应采用
大端字节序
,即低地址高字节
。 - 不管这台主机是大端机还是小端机,,都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
如果当前发送主机是小端,就需要先将数据转成大端
; 否则就忽略,直接发送即可。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
。
#include<arpa/inet.h>
uint32_t htonl(uint32_t hostlong)
uint16_t htons(uint16_t hostshort)
uint32_t ntohl(uint32_t netlong)
uint16_t ntohs(uint16_t netshort)
这些函数是网络编程中常用的函数,用于处理不同计算机系统中的字节序问题。计算机系统有两种字节序:大端字节序(Big-Endian)和小端字节序(Little-Endian)。在大端字节序中,高位字节存储在内存的低地址处,而在小端字节序中,低位字节存储在内存的低地址处。
为了在不同系统之间正确地传输数据,需要将这些数据从主机字节序转换为网络字节序,然后再从网络字节序转换回主机字节序。这就是这些函数的用途
。
-
uint32_t htonl(uint32_t hostlong)
htonl
是 “host to network long” 的缩写。- 这个函数将一个32位的无符号整数从主机字节序转换为网络字节序。
-
uint16_t htons(uint16_t hostshort)
htons
是 “host to network short” 的缩写。- 这个函数将一个16位的无符号整数从主机字节序转换为网络字节序。
-
uint32_t ntohl(uint32_t netlong)
ntohl
是 “network to host long” 的缩写。- 这个函数将一个32位的无符号整数从网络字节序转换为主机字节序。
-
uint16_t ntohs(uint16_t netshort)
ntohs
是 “network to host short” 的缩写。- 这个函数将一个16位的无符号整数从网络字节序转换为主机字节序。
这些函数在跨网络传输数据时非常有用,因为它们可以确保数据在不同的计算机系统之间正确地解释。
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
3 socket编程接口
- 可能很多人会想,怎么突然冒出个Socket呢?这个Socket是什么东西?
Socket通常被翻译为“套接字”
(是一个特殊的文件描述符,可以使用open、write、read、close进行网络通信
,Linux下一切皆文件的思想),在网络中每台服务器相当于一间房子,房子中有着不同的插口,每个插口都有一个编号,且负责某个功能。也就是说,使用不同的插口连接到对应的插口,就可以获得对应的服务。因此,Socket的含义就是两个应用程序通过一个双向的通信连接实现数据的交换,连接的一段就是一个Socket,又称为套接字。
Socket是一种抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。在网络环境中,Socket允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。套接字是通信的基石,是支持TCP/IP协议的基本操作单元
。可以将套接字看作不同主机间的进程进行双间通信的端点
,它构成了单个主机内及整个网络间的编程界面。
Socket最初是加利福尼亚大学Berkeley分校为Unix系统开发的网络通信接口。后来随着TCP/IP网络的发展,Socket成为最为通用的应用程序接口,也是在Internet上进行应用开发最为通用的API。
- 相当于Socket是将ip/tcp这些协议用编程语法包装起来,对吧?
是的,可以这样理解。Socket是将IP/TCP等协议用编程语法包装起来,为应用程序提供了更方便、更易于使用的网络通信接口
。通过使用Socket,应用程序可以更加简单地与网络中的其他应用程序进行通信,而不需要深入了解底层协议的具体实现细节。通过Socket,应用程序可以建立连接、发送和接收数据,而不需要关心底层的IP地址、端口号等细节。因此,Socket在网络通信中起到了一个重要的桥梁作用,使得应用程序能够更加高效、方便地进行网络通信。
3.1 socket 常见API
(API是应用程序编程接口
的缩写,英文全称为Application Programming Interface
,中文简称“API”)
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
这个语句相当于创建通信接口
在套接字编程中,socket
函数用于创建一个新的套接字,并返回一个套接字描述符。这个描述符可以被程序用于后续的 I/O 操作。该函数的目的是创建一个通信端点
,这个通信端点可以是基于 TCP 的,也可以是基于 UDP 的。
socket
函数的局部解释如下:
domain
: 通常为AF_INET
,表示 IPv4 地址家族。还有其他选项,例如AF_INET6
用于 IPv6。type
: 通常为SOCK_STREAM
表示 TCP,或者SOCK_DGRAM
表示 UDP。protocol
: 通常为0
,除非你有特别的原因要选择特定的协议。
所以,对于 TCP/IP 通信,这个函数是创建一个基于 TCP 的通信接口(也可以理解为套接字),并为这个套接字返回一个文件描述符。对于 UDP/IP 通信,它创建的是一个基于 UDP 的通信接口。
一旦创建了套接字,您可以使用其他的套接字函数(如 bind
, listen
, connect
, send
, recv
等)来执行各种网络 I/O 操作。
// 绑定端口号 (TCP/UDP, 服务器)
bind
函数的定义如下:
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
socket
: 这是一个之前通过socket
函数创建的套接字描述符。address
: 这是一个指向sockaddr
结构的指针,这个结构通常用于保存 IP 地址和端口号。对于 IPv4,它是一个sockaddr_in
结构;对于 IPv6,它是一个sockaddr_in6
结构。address_len
: 这是address
结构的大小,通常可以通过sizeof
运算符获得。
这个语句是用于套接字编程中的“绑定”操作,通常在服务器端使用
。
当服务器程序想要在某个特定的 IP 地址和端口上监听客户端连接时,它会使用bind
函数将套接字绑定到那个地址和端口上。这样,当有客户端尝试连接到该端口时,服务器就可以接收到连接请求。(用于将套接字接口绑定服务器,服务器一绑定,主机客户端一连接,就能够传输通信了。)
简单来说,这个语句的目的是告诉服务器:“我要在这个 IP 地址和端口上等待客户端的连接了啊
”。
// 开始监听socket (TCP, 服务器)
listen
函数的定义如下:
int listen(int socket, int backlog);
这个语句是用于套接字编程中的“监听”操作,通常在服务器端使用
。
socket
: 这是一个之前通过socket
函数创建的套接字描述符。backlog
: 这是一个整数,表示服务器在拒绝新连接之前可以排队的最大连接数。当连接数超过这个限制时,新的连接请求将会被拒绝。
当服务器程序想要开始监听某个端口上的客户端连接请求时,它会使用 listen
函数。listen
函数会将套接字设置为被动模式,等待客户端的连接请求。一旦有客户端尝试连接到该端口,服务器就会接收到一个连接请求,并可以使用 accept
函数来接受该连接。
简单来说,这个语句的目的是告诉服务器:“我要开始在这个端口上等待客户端的连接请求”。
// 接收请求 (TCP, 服务器)
accept
是一个用于处理 TCP 连接的套接字编程函数,通常在服务器端使用
。它的功能是接受一个客户端的连接请求,并返回一个新的套接字描述符,用于与该客户端进行通信
。
accept
函数的定义如下:
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
参数说明:
socket
: 这是服务器已经绑定了特定地址和端口的已存在套接字描述符。address
: 这是一个指向sockaddr
结构体的指针,用于存储客户端的地址信息。address_len
: 这是一个指向socklen_t
类型变量的指针,表示address
结构体的大小。
accept
函数会阻塞服务器套接字,直到有一个客户端尝试连接到该套接字。当有客户端连接请求时,accept
函数会返回一个新的套接字描述符,这个新的套接字描述符可以用于与该客户端进行通信。同时,address
和 address_len
参数会被填充客户端的地址信息。
注意:如果 address
和 address_len
是 NULL,那么 accept
函数会直接返回而不阻塞。这种情况下,服务器无法获取客户端的地址信息。
在成功的情况下,accept
函数返回一个新的套接字描述符;如果出现错误,则返回 -1。可以通过 perror
或其他错误处理函数来检查和处理错误。
// 建立连接 (TCP, 客户端)
connect
是一个用于处理 TCP 连接的套接字编程函数,通常在客户端使用。它的功能是向服务器发起连接请求,并建立与服务器之间的通信通道。
connect
函数的定义如下:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
sockfd
: 这是客户端已经创建的套接字描述符。addr
: 这是一个指向sockaddr
结构体的指针,用于指定服务器的地址和端口号。addrlen
: 这是一个socklen_t
类型的变量,表示addr
结构体的大小。
connect
函数会向指定的服务器地址和端口发起连接请求。如果连接成功,则可以开始通过套接字进行数据传输;如果连接失败,则返回 -1 并设置相应的错误码。
注意:connect
函数在成功时返回 0,在失败时返回 -1。可以通过 perror
或其他错误处理函数来检查和处理错误。
3.2 sockaddr结构
Socket API 是一个抽象的网络编程接口,它允许开发者在应用程序中实现网络通信,而无需关心底层的网络协议细节。这使得开发者可以更容易地编写跨平台和跨网络类型的代码。
Socket API 提供了统一的接口来使用不同的底层网络协议,如 IPv4、IPv6 和 以后会学到的UNIX Domain Sockets 等。这些底层协议具有不同的地址格式和特性
。例如,IPv4 和 IPv6 协议使用不同的地址格式,而 UNIX Domain Sockets 则使用文件系统路径来标识通信的端点。
为了处理这些不同协议的地址格式差异
,Socket API 提供了一组函数和数据结构来处理网络地址的表示和解析
。例如,sockaddr_in
结构体用于表示 IPv4 地址,而 sockaddr_in6
结构体则用于表示 IPv6 地址。这些结构体包含了用于访问和操作地址信息的字段,如端口号、IP 地址等。
开发者可以使用 Socket API 中的函数来创建套接字、绑定地址、监听连接请求、接受连接、发送和接收数据等操作。这些函数提供了灵活性和可扩展性,使得开发者可以根据需要选择合适的底层协议和地址格式来实现网络通信。
总结来说,Socket API 通过提供统一的接口来处理不同底层网络协议的地址格式差异,使得开发者可以更加方便地编写跨平台和跨网络类型的网络应用程序。
struct sockaddr {
unsigned short sa_family;
/*sa_family 是一个无符号短整型,用于表示地址族,它决定了地址的具体格式。
常见的地址族有 AF_INET(IPv4)和 AF_INET6(IPv6)。*/
char sa_data[14];
/*sa_data 是一个字符数组,
用于存储实际的地址数据,其长度通常是 14 个字节。*/
};
struct sockaddr_in {
short sin_family;
//sin_family:地址族,通常是 AF_INET
unsigned short sin_port;
//sin_port:端口号,使用网络字节序存储。
struct in_addr sin_addr;
//sin_addr:IP 地址,使用 struct in_addr 类型表示。
unsigned char sin_zero[8];
//sin_zero:用于对齐的填充字段,通常不需要使用。
};
in_addr结构
struct in_addr {
unsigned int s_addr; /* Address in network byte order */
};//in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数;
- IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
- IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
- socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;
- 虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址。
4 简单的UDP网络程序
4.1 实现一个简单的英译汉的功能
实现一个简单的英译汉的功能
备注: 代码中会用到地址转换函数
. 参考接下来的章节
- 封装UDP实现简单通信通常包括以下几个步骤:
- 创建套接字:使用 socket() 函数创建一个套接字,指定地址族为 AF_INET(IPv4),协议为 SOCK_DGRAM(UDP)和 IPPROTO_UDP。
- 绑定地址信息:使用 bind() 函数将套接字绑定到指定的地址和端口号上。如果客户端不需要绑定地址信息,可以跳过这一步。
- 发送和接收数据:使用 sendto() 函数发送数据,使用 recvfrom() 函数接收数据。这些函数需要指定目标地址作为参数,以便正确发送和接收数据。
- 关闭套接字:使用 close() 函数关闭套接字,释放资源。
封装 UdpSocket
1、udp_socket.hpp
#pragma once
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<cassert>
#include<string>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
typedef struct sockaddr sockaddr;
/*struct sockaddr 是一个已经存在的结构体类型,
通常在 <sys/socket.h> 头文件中定义。这个结构体用于
表示一个套接字地址。*/
typedef struct sockaddr_in sockaddr_in;
/*struct sockaddr_in 是一个用于表示IPv4套接字地址的结构体,
它通常在 <sys/socket.h> 或类似的头文件中定义。*/
class UdpSocket /*定义一个UDP套接字的结构体*/
{
public:
UdpSocket()
:fd_(-1)//初始化套接字的文件描述符为-1
{}
bool Socket()
{
fd_ = socket(AF_INET,SOCK_DGRAM,0);
// 使用socket系统调用创建UDP套接字并返回文件描述符.
if(fd_<0)//如果文件描述符<0,表示创建失败
{
perror("socket");//使用perror打印错误信息
return false;//返回false表示失败
}
return true;//返回true表示成功创建套接字
}
bool Close()//关闭套接字函数
{
close(_fd);//使用close系统调用关闭套接字
return true;//返回true表示成功关闭套接字
}
bool Bind(const std::string& ip,uint16_t port)
//将UDP套接字捆绑到指定的IP和端口
{
sockaddr_in addr;
//定义一个sockaddr_in结构体变量addr,用来储存IP和端口信息
addr.sin_family=AF_INET;// 设置地址族为IPv4
addr.sin_addr.s_addr=inet_addr(ip.c_str);
/*#include <arpa/inet.h> 需要的头文件
in_addr_t inet_addr(const char *cp);*/
//将字符串IP转换为32位整型数值
addr.sin_port= htons(port);
//将端口号从主机字节序转换为网络字节序
int ret = bind(fd_,(sockaddr*)&addr,sizeof(addr));
// 使用bind系统调用绑定套接字到指定IP和端口
if(ret < 0)//如果返回值小于0,表示绑定失败
{
perror("bind");// 使用perror打印错误信息
return false;// 返回false表示失败
}
return true;// 返回true表示成功绑定套接字到指定IP和端口
}
bool RecvFrom(std::string* buf,std::string* ip = NULL,uint16_t port =NULL)
// 从UDP套接字接收数据的函数
{
char tmp[1024*10]={0};// 定义一个临时缓冲区tmp,用于存储接收到的数据
sockaddr_in peer ;
// 定义一个sockaddr_in结构体变量peer,用于存储发送方的IP和端口信息
socklen_t len = sizeof(peer); // 定义peer结构体的大小
ssize_t read_size = recvfrom(fd_, tmp, sizeof(tmp) - 1, 0, (sockaddr*)&peer, &len);
// 使用recvfrom系统调用从套接字接收数据到临时缓冲区tmp中,并获取发送方的IP和端口信息
if (read_size < 0)
{
// 如果返回值小于0,表示接收数据失败
perror("recvfrom"); // 使用perror打印错误信息
return false; // 返回false表示失败
}
buf->assign(tmp, read_size); // 将接收到的数据从临时缓冲区复制到输出参数buf中。
if(ip != NULL)
{
//如果ip指针不为空
*ip = inet_ntoa(peer.sin_addr);
// 将发送方的IP地址转换为点分十进制格式的字符串并存储到ip指针指向的字符串中
}
if (port != NULL)
{ // 如果port指针不为空
*port = ntohs(peer.sin_port); // 将发送方的端口号从网络字节序转换为主机字节序并存储到port指针指向的变量中
}
return true; // 返回true表示成功接收数据
}
bool SendTo(const std::string& buf, const std::string& ip, uint16_t port)
{
// 向指定IP和端口发送数据的函数
sockaddr_in addr; // 定义一个sockaddr_in结构体变量addr,用于存储目标IP和端口信息
addr.sin_family = AF_INET; // 设置地址族为IPv4
addr.sin_addr.s_addr = inet_addr(ip.c_str());
// 将目标IP地址字符串转换为32位整型数值
addr.sin_port = htons(port);
// 将目标端口号从主机字节序转换为网络字节序
ssize_t write_size = sendto(fd_, buf.data(), buf.size(), 0, (sockaddr*)&addr, sizeof(addr));
// 使用sendto系统调用向指定IP和端口发送数据
if (write_size < 0) { // 如果返回值小于0,表示发送数据失败
perror("sendto"); // 使用perror打印错误信息
return false; // 返回false表示失败
}
return true; // 返回true表示成功发送数据
}
private:
int fd_;// 套接字的文件描述符,用于标识套接字
}
这段代码主要实现了UDP套接字的创建、绑定、接收和发送数据的功能。其中,Socket()函数用于创建UDP套接字,Close()函数用于关闭UDP套接字,Bind()函数用于将UDP套接字绑定到指定IP和端口,RecvFrom()函数用于从UDP套接字接收数据,SendTo()函数用于向指定IP和端口发送数据。同时,代码还使用了sockaddr和sockaddr_in结构体来存储网络地址信息。
UDP通用服务器
2、udp_server.hpp
#pragma once
// 这是一个预处理指令,用于防止头文件被重复包含。
#include "udp_socket.hpp"
// C 式写法
// typedef void (*Handler)(const std::string& req, std::string* resp);
// C++ 11 式写法, 能够兼容函数指针, 仿函数, 和 lamda
#include <functional> // 包含<functional>头文件,这个头文件提供了函数对象和Lambda的支持。
typedef std::function<void (const std::string&, std::string* resp)> Handler; // 使用std::function定义一个新的类型别名Handler,它可以接受一个字符串和一个字符串指针作为参数,并返回void。
class UdpServer // 定义一个名为UdpServer的类。
{
public: // 类成员的访问修饰符,表示下面的成员是公开的,可以从类的外部访问。
UdpServer() // UdpServer类的构造函数,当创建一个UdpServer对象时会自动调用。
{
assert(sock_.Socket());
// 使用assert宏来确保sock_的Socket()方法返回true。如果返回false,程序会在这里终止。
}
~UdpServer() // UdpServer类的析构函数,当UdpServer对象被销毁时会自动调用。
{
sock_.Close(); // 关闭套接字。
}
bool Start(const std::string& ip, uint16_t port, Handler handler)
// UdpServer类的一个成员函数,接受三个参数:一个字符串(IP地址)、
//一个无符号短整型(端口号)和一个函数对象(处理请求的函数)。返回一个布尔值。
{
// 1. 创建 socket // 一个注释,说明接下来的代码是创建一个socket。
// 2. 绑定端口号 // 一个注释,说明接下来的代码是绑定端口号。
bool ret = sock_.Bind(ip, port);
// 尝试将套接字绑定到指定的IP和端口号。返回值保存在ret变量中。
if (!ret) // 如果绑定失败(即ret为false)
{
return false; // 返回false。
}
// 3. 进入事件循环 // 一个注释,说明接下来的代码是进入一个无限循环,等待并处理事件。
for (;;) // 开始一个无限循环。
{
// 4. 尝试读取请求 // 一个注释,说明接下来的代码是尝试读取请求。
std::string req; // 创建一个字符串变量req来保存接收到的请求数据。
std::string remote_ip; // 创建一个字符串变量remote_ip来保存发送请求的客户端的IP地址。
uint16_t remote_port = 0;
// 创建一个无符号短整型变量remote_port来保存发送请求的客户端的端口号。初始化为0。
bool ret = sock_.RecvFrom(&req, &remote_ip, &remote_port);
// 从套接字接收数据并保存到req、remote_ip和remote_port中。返回值保存在ret变量中。
if (!ret) // 如果接收失败(即ret为false)
{
continue; // 跳过此次循环的剩余部分,进入下一次循环。
}
std::string resp; // 创建一个字符串变量resp来保存将要发送给客户端的响应数据。
// 5. 根据请求计算响应 // 一个注释,说明接下来的代码是根据接收到的请求计算响应。
handler(req, &resp); // 使用传入的handler函数对象处理请求数据req,并将结果保存到resp中。
// 6. 返回响应给客户端 // 一个注释,说明接下来的代码是将计算出的响应发送回客户端。
sock_.SendTo(resp, remote_ip, remote_port);
// 将响应数据resp发送回客户端的IP地址和端口号。
printf("[%s:%d] req: %s, resp: %s\n", remote_ip.c_str(), remote_port, req.c_str(), resp.c_str()); // 在控制台打印接收到的请求和发送的响应的信息。
}
private: // 类的私有成员部分开始。
UdpSocket sock_; // 定义一个UdpSocket类型的私有成员变量sock_,用于处理UDP套接字的操作。
}; // 类定义结束。
以上代码是对 udp 服务器进行通用接口的封装. 基于以上封装, 实现一个查字典的服务器就很容易了.
实现英译汉服务器
3、dict_server.cc
#include "udp_server.hpp"
#include <unordered_map>
#include <iostream>
std::unordered_map<std::string, std::string> g_dict;
void Translate(const std::string& req, std::string* resp)
{
auto it = g_dict.find(req);
if (it == g_dict.end()) {
*resp = "未查到!";
return;
}
*resp = it->second;
}
int main(int argc, char* argv[])
{
if (argc != 3) {
printf("Usage ./dict_server [ip] [port]\n");
return 1;
}
// 1. 数据初始化
g_dict.insert(std::make_pair("hello", "你好"));
g_dict.insert(std::make_pair("world", "世界"));
g_dict.insert(std::make_pair("c++", "最好的编程语言"));
g_dict.insert(std::make_pair("bit", "特别NB"));
// 2. 启动服务器
UdpServer server;
server.Start(argv[1], atoi(argv[2]), Translate);
return 0;
}
UDP通用客户端
4、udp_client.hpp
#pragma once
#include "udp_socket.hpp"
class UdpClient
{
public:
UdpClient(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {
assert(sock_.Socket());
}
~UdpClient() {
sock_.Close();
}
bool RecvFrom(std::string* buf) {
return sock_.RecvFrom(buf);
}
bool SendTo(const std::string& buf) {
return sock_.SendTo(buf, ip_, port_);
}
private:
UdpSocket sock_;
// 服务器端的 IP 和 端口号
std::string ip_;
uint16_t port_;
};
实现英译汉客户端
#include "udp_client.hpp" // 包含UDP客户端头文件
#include <iostream> // 包含标准输入输出库
int main(int argc, char* argv[]) { // 主函数开始,接受命令行参数
if (argc != 3) { // 如果命令行参数数量不等于3(程序名+2个参数)
printf("Usage ./dict_client [ip] [port]\n"); // 打印使用方法,说明需要IP地址和端口号两个参数
return 1; // 返回1表示程序异常退出
}
UdpClient client(argv[1], atoi(argv[2])); // 创建UDP客户端对象,使用命令行参数中的IP地址和端口号初始化
for (;;) { // 无限循环,持续等待用户输入单词并查询
std::string word; // 定义字符串变量,用于存储用户输入的单词
std::cout << "请输入您要查的单词: "; // 提示用户输入单词
std::cin >> word; // 从标准输入读取用户输入的单词
if (!std::cin) { // 如果输入失败(例如用户直接按了Enter没有输入任何内容)
std::cout << "Good Bye" << std::endl; // 打印“Good Bye”并退出循环
break; // 跳出循环
}
client.SendTo(word); // 向服务器发送用户输入的单词
std::string result; // 定义字符串变量,用于存储服务器的响应
client.RecvFrom(&result); // 从服务器接收响应,并保存到result变量中
std::cout << word << " 意思是 " << result << std::endl; // 打印查询结果
}
return 0; // 主函数返回0,表示程序正常退出
}
5 地址转换函数
本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址
,但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示
之间转换;
5.1 字符串转in_addr的函数:
#include<arpa/inet.h>
int inet_aton(const char* strptr,struct in_addr *addrptr);
in_addr_t inet_addr(const char* strptr);
int inet_pton(int family,const char* strptr,void* addrptr);
在C语言的网络编程中,arpa/inet.h 是一个包含用于网络地址操作的函数的头文件
。
1 :int inet_aton(const char *strptr, struct in_addr *addrptr)
;
-
功能:将点分十进制的IP地址字符串(例如 “192.168.1.1”)转换为一个 in_addr 结构体,该结构体中包含一个以网络字节序存储的32位整数形式的IP地址。
-
参数:
- strptr:指向包含点分十进制IP地址的字符串的指针。
- addrptr:指向 in_addr 结构体的指针,
用于存储转换后的IP地址
。
-
返回值:如果成功,返回非零值;如果失败(例如,输入的字符串不是一个有效的IP地址),返回0。
2 :in_addr_t inet_addr(const char *strptr)
;
-
功能:与 inet_aton() 类似,但是它
直接返回转换后的32位整数形式的IP地址
,而不是通过参数返回。此外,如果输入字符串无效,它返回 INADDR_NONE(通常是-1)。 -
参数:
- strptr:指向包含点分十进制IP地址的字符串的指针。
-
返回值:转换后的32位整数形式的IP地址,如果失败则返回
INADDR_NONE
。
注意:inet_addr() 在处理错误输入时不如 inet_aton() 灵活,因为它只能返回 INADDR_NONE 来表示任何类型的错误
,这使得错误处理变得困难。因此,在现代代码中,更推荐使用 inet_pton()
。
3:int inet_pton(int family, const char *strptr, void *addrptr)
;
-
功能:这是一个更现代且更通用的函数,用于将点分十进制(IPv4)或十六进制(IPv6)的IP地址字符串转换为网络地址结构。
-
参数:
- family:指定地址族,通常是 AF_INET(IPv4)或 AF_INET6(IPv6)。
- strptr:指向包含IP地址的字符串的指针。
- addrptr:指向存储转换后的网络地址的结构体的指针。
-
返回值:如果成功,返回1;
如果输入的字符串不是有效的IP地址,但格式正确(例如,对于IPv4,如果每个字段都在0到255之间,但不是有效的IP地址)
,返回0;如果输入字符串的格式不正确,返回-1,并设置 errno。
总结:在现代C语言网络编程中,建议使用 inet_pton(),因为它支持IPv4和IPv6,并且提供了更好的错误处理机制。inet_aton() 和 inet_addr() 主要用于IPv4地址,且 inet_addr() 在错误处理方面较为受限。
5.2 in_addr转字符串的函数:
#include<arpa/inet.h>
char* inet_ntoa(struct in_addr inaddr);
const char* inet_ntop(int family,const void* addrptr,char* strptr,size_t len);
1: inet_ntoa
是一个用于将网络地址结构(通常是一个 in_addr
结构体)转换为点分十进制字符串的函数。该函数主要用于将IPv4地址转换为可读的字符串格式。
函数的原型是:
char* inet_ntoa(struct in_addr inaddr);
参数:
inaddr
:一个in_addr
结构体,其中包含了要转换的IPv4地址。
返回值:
- 如果成功,该函数返回一个指向点分十进制格式的字符串的指针。这个字符串是一个静态分配的,这意味着你不应该试图释放它或修改它。
- 如果发生错误,该函数返回
NULL
。
使用示例:
#include <stdio.h>
#include <arpa/inet.h>
int main() {
struct in_addr addr;
addr.s_addr = htonl(0x7f000001); // 127.0.0.1 的网络地址
char* str = inet_ntoa(addr);
printf("IP address: %s\n", str); // 输出 "IP address: 127.0.0.1"
return 0;
}
注意:此函数在现代代码中可能不常用,因为其返回的字符串是静态分配的,这可能导致不可预测的行为。现代的代码更倾向于使用 inet_ntop
或其他更现代的函数。
2:inet_ntop
是一个用于将网络地址结构转换为字符串的函数,它是 inet_ntoa
的现代替代品。与 inet_ntoa
相比,inet_ntop
提供了更多的灵活性,并支持多种地址族。
函数的原型是:
const char* inet_ntop(int family, const void* addrptr, char* strptr, size_t len);
参数:
family
:地址族,例如AF_INET
或AF_INET6
。addrptr
:指向要转换的网络地址结构的指针。strptr
:指向一个字符数组的指针,该数组用于存储转换后的字符串。len
:字符数组的长度。
返回值:
- 如果成功,该函数返回一个指向转换后的字符串的指针。
- 如果发生错误,该函数返回
NULL
。
使用示例(IPv4):
#include <stdio.h>
#include <arpa/inet.h>
int main() {
struct in_addr addr;
addr.s_addr = htonl(0x7f000001); // 127.0.0.1 的网络地址
const char* str = inet_ntop(AF_INET, &addr, " ", sizeof(" "));
printf("IP address: %s\n", str); // 输出 "IP address: 127.0.0.1"
return 0;
}
使用示例(IPv6):
#include <stdio.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
int main() {
struct in6_addr addr;
memcpy(&addr, &in6addr_loopback, sizeof(in6addr_loopback)); // ::1 的网络地址
const char* str = inet_ntop(AF_INET6, &addr, " ", sizeof(" "));
printf("IP address: %s\n", str); // 输出 "IP address: ::1"
return 0;
}
注意:在使用 inet_ntop
时,确保目标字符串有足够的空间来存储转换后的字符串,以避免缓冲区溢出
。
6 简单的TCP网络程序
以下是一个简单的TCP网络程序的示例,包括一个服务器和一个客户端。
服务器端代码:
// 引入必要的头文件
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int main() {
// 创建TCP/IP套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
std::cerr << "创建套接字失败" << std::endl;
return -1;
}
// 绑定套接字到本地地址和端口
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(12345);
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
std::cerr << "绑定套接字失败" << std::endl;
close(sockfd);
return -1;
}
// 监听连接
if (listen(sockfd, 1) < 0) {
std::cerr << "监听连接失败" << std::endl;
close(sockfd);
return -1;
}
// 等待客户端连接请求并处理请求
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int connfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);
if (connfd < 0) {
std::cerr << "接受连接请求失败" << std::endl;
close(sockfd);
return -1;
}
std::cout << "连接来自:" << inet_ntoa(client_addr.sin_addr) << std::endl;
char buffer[1024];
int n = read(connfd, buffer, sizeof(buffer));
std::cout << "收到:" << buffer << std::endl;
n = write(connfd, buffer, n); // 发送数据给客户端,实现回显功能
close(connfd); // 关闭连接,释放资源
close(sockfd); // 关闭套接字,释放资源
return 0; // 程序正常退出
}
客户端代码:
// 引入必要的头文件
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int main() {
// 创建TCP/IP套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
std::cerr << "创建套接字失败" << std::endl;
return -1;
}
// 连接服务器
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(12345);
if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0) {
std::cerr << "服务器地址转换失败" << std::endl;
close(sockfd);
return -1;
}
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
std::cerr << "连接服务器失败" << std::endl;
close(sockfd);
return -1;
}
std::cout << "连接服务器成功" << std::endl;
// 发送和接收数据
char buffer[1024];
std::cout << "请输入要发送的消息:" << std::endl;
std::cin >> buffer;
write(sockfd, buffer, strlen(buffer)); // 发送数据给服务器,实现回显功能
int n = read(sockfd, buffer, sizeof(buffer)); // 从服务器接收数据并显示在屏幕上
std::cout << "收到:" << buffer << std::endl;
close(sockfd); // 关闭套接字,释放资源
return 0; // 程序正常退出
}
7 简单的TCP网络程序(多进程版本)
当涉及到多进程的 TCP 网络编程时,可以使用 fork() 函数创建子进程来处理客户端连接。以下是一个简单的 C++ TCP 网络程序的多进程版本示例:
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
void processConnection(int clientSocket) {
// 处理客户端连接的代码逻辑
// 在这里可以进行数据收发等操作
// 例如:
char buffer[1024];
int bytesRead = read(clientSocket, buffer, sizeof(buffer));
if (bytesRead > 0) {
std::cout << "Received data from client: " << buffer << std::endl;
}
// ...
// 关闭客户端套接字
close(clientSocket);
}
int main() {
// 创建套接字
int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket == -1) {
std::cerr << "Failed to create socket." << std::endl;
return 1;
}
// 绑定地址和端口
sockaddr_in serverAddress{};
serverAddress.sin_family = AF_INET;
serverAddress.sin_addr.s_addr = INADDR_ANY;
serverAddress.sin_port = htons(8080);
if (bind(serverSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress)) == -1) {
std::cerr << "Failed to bind." << std::endl;
close(serverSocket);
return 1;
}
// 监听连接请求
if (listen(serverSocket, SOMAXCONN) == -1) {
std::cerr << "Failed to listen." << std::endl;
close(serverSocket);
return 1;
}
while (true) {
// 接受客户端连接
sockaddr_in clientAddress{};
socklen_t clientAddressLength = sizeof(clientAddress);
int clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddress, &clientAddressLength);
if (clientSocket == -1) {
std::cerr << "Failed to accept client connection." << std::endl;
close(serverSocket);
return 1;
}
// 创建子进程处理客户端连接
pid_t pid = fork();
if (pid == -1) {
std::cerr << "Failed to create child process." << std::endl;
close(clientSocket);
continue;
}
if (pid == 0) {
// 子进程
close(serverSocket); // 子进程不需要监听套接字
processConnection(clientSocket);
return 0;
} else {
// 父进程
close(clientSocket); // 父进程不需要客户端套接字
}
}
// 关闭服务器套接字
close(serverSocket);
return 0;
}
上述代码中,主要的逻辑是在一个无限循环中接受客户端连接并创建子进程处理连接。父进程负责接受连接请求,创建子进程后继续等待新的连接请求。子进程则负责处理客户端连接,可以在 processConnection()
函数中编写具体的处理逻辑。
请注意,这只是一个简单的示例,没有进行错误处理和异常处理。在实际应用中,还需要考虑更多的细节,如错误处理、信号处理等。另外,多进程模型不是唯一的网络编程模型,还可以使用多线程或异步编程等方式来实现。
8 简单的TCP网络程序(多线程版本)
以下是一个使用C++编写的简单的多线程TCP服务器程序的示例:
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
#include <unistd.h>
using namespace std;
const int MAX_CLIENTS = 10; // 最大客户端数量
const int BUFFER_SIZE = 1024; // 缓冲区大小
// 线程处理函数
void *handleClient(void *arg) {
int clientSocket = *(int *)arg;
free(arg); // 释放指针
char buffer[BUFFER_SIZE];
int n = read(clientSocket, buffer, BUFFER_SIZE);
if (n > 0) {
cout << "收到:" << buffer << endl;
n = write(clientSocket, buffer, n); // 发送数据给客户端,实现回显功能
}
close(clientSocket); // 关闭连接,释放资源
pthread_exit(NULL); // 线程结束
}
int main() {
int serverSocket, clientSocket;
struct sockaddr_in serverAddr, clientAddr;
socklen_t clientLen = sizeof(clientAddr);
pthread_t tid;
int *clientSockets = new int[MAX_CLIENTS]; // 存储客户端套接字数组
int clientCount = 0; // 当前客户端数量
// 创建TCP/IP套接字并绑定到本地地址和端口
serverSocket = socket(AF_INET, SOCK_STREAM, 0);
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(12345);
bind(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
// 监听连接请求
listen(serverSocket, MAX_CLIENTS);
while (true) {
// 等待客户端连接请求并接受连接请求,存储客户端套接字到数组中,并创建新线程处理客户端请求
clientSocket = accept(serverSocket, (struct sockaddr *)&clientAddr, &clientLen);
if (clientCount < MAX_CLIENTS) {
clientSockets[clientCount] = clientSocket;
pthread_create(&tid, NULL, handleClient, (void *)&clientSocket); // 创建新线程处理客户端请求
clientCount++;
} else {
close(clientSocket); // 客户端数量已达上限,关闭连接,释放资源
}
}
// 等待所有线程结束,释放资源并关闭套接字
while (clientCount > 0) {
pthread_join(tid, NULL); // 等待线程结束
close(clientSockets[--clientCount]); // 关闭客户端套接字,释放资源
}
close(serverSocket); // 关闭服务器套接字,释放资源
delete[] clientSockets; // 释放动态分配的内存空间
return 0; // 程序正常退出
}
在上面的代码中,我们创建了一个简单的TCP服务器程序,该程序可以同时处理多个客户端连接。它使用多线程来处理客户端请求,并使用套接字编程接口进行网络通信。
当服务器程序运行时,它会创建一个TCP/IP套接字并绑定到本地地址和端口。然后,它开始监听连接请求,并在收到客户端连接请求时接受连接请求。客户端套接字被存储在一个数组中,并创建新线程来处理客户端请求。
在每个线程中,程序从客户端套接字读取数据,并将其发送回客户端,实现回显功能。然后关闭客户端套接字,释放资源。
当所有线程结束时,程序释放动态分配的内存空间并关闭套接字。
请注意,这只是一个简单的示例程序,实际的应用程序可能需要更多的错误处理和资源管理逻辑。此外,为了使程序更加健壮和可靠,您可能需要添加更多的功能和特性,例如超时处理、并发限制、日志记录等。
9 TCP协议通讯流程
下图是基于TCP协议的客户端/服务器程序的一般流程:
服务器初始化:
- 调用socket, 创建文件描述符;
- 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
- 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
- 调用accecpt, 并阻塞, 等待客户端连接过来;
建立连接的过程:
- 调用socket, 创建文件描述符;
- 调用connect, 向服务器发起连接请求;
- connect会发出SYN段并阻塞等待服务器应答; (第一次)
- 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
- 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
这个建立连接的过程, 通常称为 三次握手
;
数据传输的过程:
- 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
- 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
- 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
- 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
- 客户端收到后从read()返回, 发送下一条请求,如此循环下去;
断开连接的过程:
- 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
- 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
- read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
- 客户端收到FIN, 再返回一个ACK给服务器; (第四次)
这个断开连接的过程, 通常称为 四次挥手
在学习socket API时要注意应用程序和TCP协议层是如何交互的:
- 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段;
- 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。
10 TCP 和 UDP 对比
TCP(传输控制协议)和UDP(用户数据报协议)是两种主要的网络传输协议,它们在以下几个方面存在一些显著的区别:
- 连接性:TCP是一种面向连接的协议,数据传输之前需要先建立一个连接。而UDP是无连接的协议,发送数据前不需要事先建立连接。
- 可靠性:TCP提供了数据传输的确认和重传机制,通过序列号对数据进行排序和校验,确保数据的完整性、正确性和顺序性。而UDP则没有这样的保证,可能会出现数据丢失、重复或乱序的情况。
- 速度和效率:由于TCP需要进行连接建立和确认重传等操作,因此在数据传输速度上可能较慢。而UDP则没有这些开销,因此在数据传输速度上可能更快。此外,由于UDP没有流量控制和拥塞控制机制,因此在网络拥堵的情况下,UDP可能会表现出更高的效率。
- 数据包大小:TCP将数据划分为较小的数据包进行传输,并根据网络状况进行调整。而UDP则没有这样的限制,可以发送任意大小的数据包。
- 应用场景:TCP适用于对数据可靠性要求较高的应用场景,如网页浏览、电子邮件、文件传输等。而UDP适用于对实时性要求较高的应用场景,如流媒体、实时游戏、VoIP(语音通话)等。
综上所述,TCP和UDP各有其特点和使用场景。在需要可靠、有序和错误校验的数据传输时,可以选择使用TCP;而在需要快速、实时的数据传输时,可以选择使用UDP。
11 在一个通信过程中,是使用IP/TCP协议簇的一部分,还是任何通信都使用所有的IP/TCP协议呢?
对于一个通信过程,通常不会使用TCP/IP协议簇中的所有协议
。而是根据通信的需求和网络层次结构,只使用其中一部分协议
。
例如,在传输层中,可以选择使用TCP协议或
UDP协议,而不是同时使用两者。同样,在网络层中,可以选择使用IP协议,但是一般不会同时使用IPv4和IPv6协议
。
另外,需要注意的是,TCP/IP协议簇中的各个协议都是相互独立的
,它们之间并没有强制性的依赖关系。因此,在实际的网络通信过程中,可以根据需要选择合适的协议组合,以满足不同通信需求。
综上所述,一个通信过程并不会使用TCP/IP协议簇中的所有协议,而是根据实际需求选择其中的一部分协议。