理解网络编程和套接字
(一)网络编程
网络编程的核心在于编写程序使两台联网的计算机相互交换数据。虽然计算机之间传输数据首先需要物理连接,但鉴于绝大部分计算机都已连接到庞大的互联网,这一基础条件通常无需担忧。因此,开发的重点在于如何编写数据传输软件,而操作系统为此提供了名为“套接字”的关键部件,使得数据交换成为可能。
(二)套接字
套接字是网络数据传输用的软件设备,也是网络编程中的一种基本通信机制。它用于在网络中建立、管理和终止计算机之间的连接,为应用程序提供了一种在计算机网络中进行数据传输的标准接口。通过套接字,程序可以利用网络协议(如TCP/IP)与其他套接字进行高效通信,从而实现跨网络的数据交互。
构建套接字
在大多数项目实践中,服务器端通常在 Linux 系列的操作系统下开发,而客户端则多在 Windows 平台下开发。有时应用程序还需要在两个平台之间相互切换,因此有必要兼顾 Windows 和 Linux 两大平台,确保代码的兼容性与跨平台能力。
(一)Linux 平台下套接字的构建步骤
服务器端
① 调用 socket 函数创建套接字。
#include
// 成功时返回文件描述符,失败时返回-1
int socket(int domain, int type, int protocol);
② 调用 bind 函数分配 IP 地址和端口号。
#include
// 成功时返回0,失败时返回-1
int bind(int sockfd, struct sockaddr* myaddr, socklen_t addrlen);
③ 调用 listen 函数转为可接收请求状态。
#include
// 成功时返回0,失败时返回-1
int listen(int sockfd, int backlog);
④ 调用 accept 函数受理连接请求。
#include
// 成功时返回文件描述符,失败时返回-1
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
客户端
① 调用 socket 函数创建套接字。
此步骤与服务器端相同。
② 调用 connect 函数向服务器端发送连接请求。
#include
// 成功时返回0,失败时返回-1
int connect(int sockfd, struct sockaddr* serv_addr, socklen_t addrlen);
(二)Windows 平台下套接字相关函数
① socket 函数
#include
// 成功时返回套接字句柄,失败时返回 INVALID_SOCKET
SOCKET socket(int af, int type, int protocol);
② bind 函数
#include
// 成功时返回0,失败时返回 SOCKET_ERROR
int bind(SOCKET s, const struct sockaddr* name, int namelen);
知识点
(1)套接字在网络编程中的作用是什么?为何称它为套接字?
套接字的作用
套接字是网络编程中用于通信的端点,它允许不同计算机上的进程进行数据交换。通过套接字,应用程序可以建立连接、发送和接收数据,从而实现网络通信。
为何称它为套接字?
“套接字”(Socket)一词源于其形象化的比喻。在网络通信中,套接字就像是一个“插座”,而网络协议(如 TCP/IP)则是“插头”。当两个进程需要通信时,它们通过套接字建立连接,就像将插头插入插座一样,从而实现数据的传输。
套接字的类型
- 流式套接字(SOCK_STREAM):提供面向连接的、可靠的数据传输服务,通常用于 TCP 协议。
- 数据报套接字(SOCK_DGRAM):提供无连接的、不可靠的数据传输服务,通常用于 UDP 协议。
- 原始套接字(SOCK_RAW):允许直接访问底层网络协议,通常用于实现自定义协议或网络诊断工具。
套接字的工作流程
- 创建套接字:使用
listen函数创建套接字。 - 绑定地址:使用
accept函数将套接字绑定到特定的 IP 地址和端口。 - 监听连接:对于服务器端,使用
connect函数监听来自客户端的连接请求。 - 接受连接:服务器端使用
closesocket函数接受客户端的连接请求。 - 数据传输:通过
accept和gcc函数进行数据的发送和接收。 - 关闭套接字:通信结束后,使用
g++函数关闭套接字,释放资源。
示例代码说明
在上述示例中,服务器端创建了一个套接字,并绑定到指定端口,等待客户端连接。当客户端连接请求到达时,服务器接受连接并发送一条消息。客户端则连接到服务器,接收消息并打印输出。
跨平台差异
- Linux 平台:使用标准的
-l函数进行套接字操作。 - Windows 平台:除了标准的
-l函数外,还需要使用socket()和bind()函数进行初始化和清理工作。
通过理解套接字的工作原理和操作流程,可以更好地掌握网络编程的基本概念,为后续更复杂的网络应用开发打下基础。
网络通信的核心机制
在网络编程中,理解套接字的本质及其工作原理至关重要。以下从五个关键维度深入解析套接字的功能与特性:
1. 网络通信的端点
套接字是网络通信的基本端点,它允许程序通过网络与其他计算机或设备进行数据交换。它提供了一个标准化的通信接口,使得应用程序能够方便地通过网络发送和接收信息,从而实现了不同进程间的远程通信。
2. 抽象和简化
套接字为网络编程提供了一个统一的接口,屏蔽了底层的网络协议和复杂的实现细节。程序员可以通过标准的套接字API(如socket(), bind(), listen(), accept(), send(), recv(), close())进行操作,而无需深入了解底层协议的具体实现,极大地降低了开发难度。
3. 支持不同协议
套接字可以使用不同的协议进行通信,最常见的是TCP(面向连接)和UDP(无连接)。不同的套接字类型支持不同的通信模式:
- 流式套接字(Stream Socket):通常基于TCP,提供可靠的、面向连接的通信,确保数据按顺序无差错地到达。
- 数据报套接字(Datagram Socket):通常基于UDP,提供无连接的、尽力而为的通信,适用于对实时性要求较高但可容忍少量丢包的场景。
4. 建立和管理连接
套接字用于建立、维护和终止网络连接。服务器端使用套接字监听连接请求,客户端使用套接字发起连接。连接建立后,数据可以通过套接字进行双向传输,直到一方主动关闭连接。
5. 数据传输
套接字提供了数据的发送和接收功能。应用程序可以使用套接字发送数据到远程主机,并接收来自远程主机的数据。这种机制使得网络通信变得直观且易于实现。
为什么称其为“套接字”?
套接字(Socket)这个词可以理解为一个连接点或插口,它在网络编程中充当了两个通信实体之间的接头部分。正如电气工程中的插头和插座一样,网络套接字提供了连接网络上不同计算机的接口,使得数据能够像电流通过线路一样在网络中流动。
(2)服务器端创建套接字后,会依次调用 listen 函数和 accept 函数,请比较并说明二者区别
| 特点 | listen | accept |
|---|---|---|
| 作用 | 将套接字设置为监听状态,等待连接请求。 | 从监听队列中取出一个连接请求,并返回一个新的套接字。 |
| 调用时机 | 在bind之后、accept之前调用。 | 在listen之后调用。 |
| 返回值 | 无返回值,函数成功时返回0,失败时返回-1。 | 返回新的套接字描述符用于与客户端通信,失败时返回-1。 |
| 处理方式 | 标记套接字为可接受连接的状态。 | 提取一个连接请求并为其创建新的套接字。 |
| 状态 | 进入监听状态,等待客户端连接。 | 为每个客户端连接创建一个新的套接字。 |
(3)Linux中,对套接字数据进行I/O时可以直接使用文件I/O相关函数,Windows中则不可以,原因为何?
Linux中的套接字I/O与文件I/O
在Linux中,套接字和文件都被视为文件描述符(file descriptors)。这种设计理念被称为“Everything is a file”(一切皆文件)。具体来说:
-
统一的I/O接口:
- 在Linux中,套接字、文件、管道、设备等都使用相同的文件描述符接口。即,套接字和普通文件使用相同的系统调用接口(如
read()、write()、close()等)进行I/O操作。 - 例如,套接字的读写操作可以直接使用
read()和write()函数,这与操作文件的方式是相同的。
- 在Linux中,套接字、文件、管道、设备等都使用相同的文件描述符接口。即,套接字和普通文件使用相同的系统调用接口(如
-
原因:
- 设计简化:这种设计简化了I/O操作的实现和使用,因为操作系统的I/O模型对所有资源类型都是一致的。
- 抽象层:操作系统通过文件描述符抽象了底层硬件和资源,使得不同类型的I/O操作可以通过相同的接口进行处理。
Windows中的套接字I/O与文件I/O
在Windows中,套接字的I/O操作与文件的I/O操作是分开的。Windows使用了不同的API来处理套接字和文件:
(4)创建套接字后一般会给它分配地址,为什么?为了完成地址分配需要调用哪个函数?
为什么需要分配地址
-
标识网络节点:
- 每个网络节点(如服务器或客户端)在网络上需要一个唯一的地址,以便其他节点能够找到它并进行通信。这个地址通常包括IP地址和端口号。
-
接收数据:
- 套接字在网络上接收数据时,必须绑定到一个特定的地址。这样,操作系统才能将到达的数据包正确地路由到相应的套接字。
-
网络通信:
- 对于服务器端,分配地址使服务器能够监听来自客户端的连接请求。对于客户端,分配地址(通常是动态分配的)使客户端能够通过特定的端口与服务器通信。
地址分配所需的函数
bind 函数:
在Linux系统中,创建套接字后,通常需要调用 bind 函数来将套接字与特定的IP地址和端口号绑定。该函数的原型通常如下:
int
bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
其中,sockfd 是套接字的文件描述符,addr 是指向地址结构体的指针,addrlen 是地址结构体的长度。通过调用此函数,可以将套接字绑定到指定的网络接口和端口,从而允许其他节点通过该地址与套接字进行通信。
需要注意的是,对于客户端套接字,通常不需要显式调用 bind 函数,因为操作系统会在连接时自动分配一个临时端口。然而,对于服务器套接字,显式绑定地址是必要的,以便服务器能够监听特定的端口并接收来自客户端的连接请求。
此外,绑定地址时还需要注意地址结构的正确性。例如,对于IPv4套接字,需要使用 struct sockaddr_in 结构体,并正确设置 sin_family、sin_addr 和 sin_port 等字段。对于IPv6套接字,则需要使用 struct sockaddr_in6 结构体。
通过正确调用 bind 函数,可以确保套接字能够正确地与网络地址关联,从而实现稳定的网络通信。
6/6
绑定套接字与本地地址
在建立网络连接的过程中,bind函数扮演着至关重要的角色。该函数的主要功能是将一个套接字与一个本地地址紧密绑定在一起,这个本地地址通常由IP地址和端口号组成。这是完成地址分配的关键步骤,只有成功绑定后,套接字才能被操作系统识别并用于后续的通信操作。如果跳过这一步,套接字将无法正确接收来自特定网络接口和端口的数据,从而导致连接失败。
(5)Linux中的文件描述符与Windows的句柄实际上非常类似,请以套接字为对象说明它们的含义。
文件描述符与句柄的比较
为了深入理解网络编程的基础,我们需要明确
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数的参数构成。这些参数直接决定了绑定的成功与否:
sockfd:这是套接字描述符,它是通过socket函数创建得到的。该描述符是操作系统分配给套接字的唯一标识,后续所有对该套接字的操作都需依赖此描述符。addr:这是一个指向sockaddr结构体的指针。该结构体中包含了详细的地址信息,如协议族、IP地址和端口号,指定了套接字需要绑定的具体网络地址。addrlen:这表示addr结构体的大小。在传递地址信息时,操作系统需要知道结构体的字节长度,以便正确解析其中的数据,确保地址信息的完整性和准确性。
文件描述符与句柄的比较
在跨平台网络编程中,理解不同操作系统的资源标识方式至关重要。
| 特性 | 文件描述符(Linux) | 句柄(Windows) |
|---|---|---|
| 类型 | 非负整数 | 指针类型(实际上是一个整数,但作为抽象引用) |
| 创建 | socket() 函数返回文件描述符 | socket() 函数返回句柄 |
| 关闭 | 使用 close() 函数 | 使用 closesocket() 函数 |
| 访问 | 通过整数索引访问文件或网络套接字 | 通过句柄引用访问系统对象 |
| 抽象 | 抽象为一个简单的整数索引 | 抽象为一个指向系统对象的引用 |
- 文件描述符和句柄都用于标识和访问系统中的I/O资源,包括套接字。在Linux中,文件描述符是一个简单的非负整数,用于索引打开的文件或套接字。这种设计简洁高效,便于底层系统调用。而在Windows中,句柄是一种抽象的引用,用于标识系统对象,包括文件和套接字。句柄可能包含更多的元数据,以支持Windows复杂的对象管理系统。
- 虽然在实现上有所不同,两个概念都为操作系统提供了一种统一的方式来管理和访问各种资源。在应用程序编程中,文件描述符和句柄都扮演着关键角色,使得程序能够有效地进行网络通信和文件操作。开发者需要根据目标平台选择合适的抽象层,以确保代码的可移植性和性能。
#include
// 成功时返回0,失败时返回 SOCKET_ERROR
int listen(SOCKET s, int backlog);
#include
// 成功时返回套接字句柄,失败时返回 INVALID_SOCKET
int accept(SOCKET s, struct sockaddr* addr, int* addrlen);
#include
// 成功时返回0,失败时返回 SOCKET_ERROR
int connect(SOCKET s, const struct sockaddr* name, int namelen);
#include
// 成功时返回0,失败时返回 SOCKET_ERROR
int closesocket(SOCKET s);
#include
#include
#include
#include
#include
#include
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('n', stderr);
exit(1);
}
int main(int argc, char *argv[])
{
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
char message[] = "Hello World!";
if(argc != 2)
{
printf("Usage: %s n" , argv[0]);
exit(1);
}
// 调用 socket 函数创建套接字
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
// 调用 bind 函数分配 IP 地址和端口号
if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
// 调用 listen 函数将套接字转为可接收连接状态
if(listen(serv_sock, 5) == -1)
error_handling("listen() error");
clnt_addr_size = sizeof(clnt_addr);
// 调用 accept 函数受理连接请求。如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止
clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size);
if(clnt_sock == -1)
error_handling("accept() error");
// write 函数用于传输数据
write(clnt_sock, message, sizeof(message));
close(clnt_sock);
close(serv_sock);
return 0;
}
#include
#include
#include
#include
#include
#include
void error_handling(const char* message)
{
fputs(message, stderr);
fputc('n', stderr);
exit(1);
}
int main(int argc, char* argv[])
{
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len;
if(argc != 3)
{
printf("Usage: %s n" , argv[0]);
exit(1);
}
// 创建套接字,但此时套接字并不马上分为服务器端和客户端。
// 如果紧接着调用 bind、lsiten 函数,将成为服务器端套接字;
// 如果调用 connect 函数,将成为客户端套接字。
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
// 调用 connect 函数向服务器端发送连接请求
if(connect(sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error!");
str_len = read(sock, message, sizeof(message) - 1);
if(str_len == -1)
error_handling("read() error!");
printf("Message from server : %s n", message);
close(sock);
return 0;
}
#include
#include
#include
void ErrorHandling(char* message)
{
fputs(message, stderr);
fputc('n', stderr);
exit(1);
}
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
SOCKADDR_IN servAddr, clntAddr;
int szClntAddr;
char message[] = "Hello World!";
if(argc != 2)
{
printf("Usage : %sn" , argv[0]);
system("pause");
exit(1);
}
// 初始化套接字库
if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error!");
// 创建套接字
hServSock = socket(PF_INET, SOCK_STREAM, 0);
if(hServSock == INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(atoi(argv[1]));
// 给套接字分配IP地址与端口号
if(bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
ErrorHandling("bind() error");
// 调用listen函数使创建的套接字成为服务器端套接字
if(listen(hServSock, 5) == SOCKET_ERROR)
ErrorHandling("listen() error");
szClntAddr = sizeof(clntAddr);
// 受理客户端连接请求
hClntSock = accept(hServSock,(SOCKADDR*)& clntAddr, &szClntAddr);
if (hClntSock == INVALID_SOCKET)
ErrorHandling("accept() error");
// 向连接的客户端传输数据
send(hClntSock, message, sizeof(message), 0);
closesocket(hClntSock);
closesocket(hServSock);
// 程序终止前注销初始化的套接字库
WSACleanup();
system("pause");
return 0;
}
#include
#include
#include
void ErrorHandling(char* message)
{
fputs(message, stderr);
fputc('n', stderr);
exit(1);
}
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN servAddr;
char message[30];
int strLen;
if (argc != 3)
{
printf("Usage : %s n" , argv[0]);
exit(1);
}
// 初始化Winsock库
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WASStartup() error!");
// 创建套接字
hSocket = socket(PF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
ErrorHandling("socket() error!");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr(argv[1]);
servAddr.sin_port = htons(atoi(argv[2]));
// 通过套接字向服务器端发出连接请求
if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
ErrorHandling("connect() error!");
// 接收服务器发来的数据
strLen = recv(hSocket, message, sizeof(message) - 1, 0);
if (strLen == -1)
ErrorHandling("read() error!");
printf("Message from server: %s n", message);
closesocket(hSocket);
// 注销初始化的Winsock库
WSACleanup();
return 0;
}
recv()
send()
closesocket()
ReadFile()
WriteFile()
CloseHandle()









