本文主要涉及
- 网络编程的基础概念:IP地址,以及典型的两种通信方式TCP与UDP;
- 网络编程从创建套接字到进行数据传输的各函数的参数详细解释
为了在两台计算机之间进行通信,它们需要在同一个网络(例如,局域网)中,并且每台计算机都需要有一个唯一的标识符,通常是IP地址。
以下是更详细的解释:
IP地址:IP地址是网络中设备的标识符,它允许数据在网络上进行路由。每台计算机或设备(例如,智能手机、服务器等)在网络上都必须有一个唯一的IP地址。在IPv4协议中,IP地址通常是一个32位的数字,如192.168.1.1
。在IPv6协议中,IP地址更长,并采用了128位的地址空间,如2001:0db8:85a3:0000:0000:8a2e:0370:7334
。
局域网(LAN):局域网是一个较小的网络,通常覆盖一个建筑或者一个组织的办公室。在局域网中的设备可以直接互相通信,无需通过互联网。例如,一个家庭网络或者公司内部的网络都可以是局域网。
路由:当两台计算机在同一个局域网中,并且有各自的IP地址后,它们可以直接互相通信。但是,如果两台计算机位于不同的局域网或网络上,那么它们需要一个路由器或者网关来转发数据。路由器负责决定如何将数据从源地址传输到目标地址。
IPV4 --> 4个字节来表示一台主机
IPV4资源紧张–>扩充到IPV6–>8个字节
IPV4 4个字节,如果一个局域网内的所有IP分完,那么可以分配给2^32个
IPv4 (Internet Protocol version 4):
.
分隔,例如:192.168.1.1。IPv6 (Internet Protocol version 6):
:
分隔,例如:2001:0db8:85a3:0000:0000:8a2e:0370:7334。总的来说,IPv6被视为解决IPv4地址空间不足问题的长期解决方案。随着时间的推移和互联网的发展,IPv6的采用率正在逐渐增加,但在全球范围内,IPv4和IPv6可能会长时间共存,直到所有设备和网络都能完全迁移到IPv6。
在世界范围来看,局域网的数量也很多,在互联网上是不通过局域网之内的用户进行通信的,因此在找另外一台主机时,除去区分这个主机以外还需要区分局域网。
所以我们的IP地址分为:网络号+主机号
网络号:用来标识局域网;
主机号:标识局域网中的主机。
要区分哪些是网络号与主机号那就需要子网掩码
.
子网掩码:
可以没有0,也没有1
11111111 11111111 11111111 11111111 --> 全是网络号没有主机没有意义
全零–> 全是主机,没有网络,是可以存在的,是世界上最大的网
IP地址的基本组成:
IP地址的分类:
根据IP地址的高阶位,IP地址被分为以下几类:
大型网络
。中等大小的网络
。型网络或子网
。多点广播
。未来使用或实验目
的。子网掩码:
CIDR (Classless Inter-Domain Routing):
当两台计算机在互联网上通信时,路由器或交换机会根据IP地址和子网掩码来确定目标地址是在本地网络还是需要通过路由器进行转发。这就是为什么需要网络号和主机号的原因。
一台电脑里面可能会运行多个网络程序,但对应的网卡都是一个。
因为IP地址都是公用的,所以需要一个机制去区分网络程序---端口。
网络程序= IP+端口
在计算机网络中,端口号用于标识在一个特定的IP地址上运行的应用程序。这是因为一个计算机可以运行多个网络服务或应用程序,而每个服务或应用程序都需要一个独特的标识符,这就是端口号。
知名端口 (Well-Known Ports): 端口号范围从0到1023。这些端口号被众所周知,并且通常用于标准服务,如HTTP(端口80)、HTTPS(端口443)、FTP(端口21)、SSH(端口22)等。这些端口号是为特定的服务保留的,并且在大多数系统上都被固定地分配给了特定的服务。
注册端口 (Registered Ports): 端口号范围从1024到49151。这些端口号可以被应用程序自由使用,只要它们不与已知的服务冲突。它们是为那些不是标准服务但仍然需要网络连接的应用程序保留的。
动态/私有端口 (Dynamic/Private Ports): 端口号范围从49152到65535。这些端口号可以被客户端程序用作源端口,当它们发起与远程服务的通信时。这些端口号不会在系统上被预分配给特定的服务或应用程序。
通信有两种:TCP与UDP
TCP(传输控制协议)和UDP(用户数据报协议)两种不同的传输层协议的基本操作流程
TCP是一个面向连接的协议,它确保了数据的可靠传输。以下是你描述的TCP通信的步骤:
服务端:
客户端:
UDP是一个无连接的协议,它不保证数据的可靠传输,但传输效率较高。以下是你描述的UDP通信的步骤:
服务端:
客户端:
总之,TCP提供了可靠的数据传输机制,适用于那些需要确保数据完整性和顺序的应用场景。而UDP则更适合那些需要快速传输、实时性要求高,但可以容忍数据丢失的应用场景。
man 2 socket
NAME
socket - create an endpoint for communication
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
NAME: 显示了系统调用的名称,这里是 socket
。
SYNOPSIS: 给出了函数的原型或声明。
#include <sys/types.h>
: 这是一个C预处理器指令,用于包含类型定义。#include <sys/socket.h>
: 这是一个C预处理器指令,用于包含socket函数的声明。int socket(int domain, int type, int protocol);
: 这是 socket
函数的原型。它接受三个参数并返回一个整数值。参数解释:
AF_INET
表示使用IPv4协议,AF_INET6
表示使用IPv6协议。其他常见值包括 AF_UNIX
或 AF_LOCAL
(用于本地通信)。SOCK_STREAM
表示流套接字(通常用于TCP),而 SOCK_DGRAM
表示数据报套接字(通常用于UDP)。NAME 名称
bind - 将一个名字和一个套接字绑定到一起(赋一个名字给一个套接字)
SYNOPSIS 概述
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
DESCRIPTION 描述
bind 为套接字 sockfd
指定本地地址 my_addr
. my_addr 的长度为 addrlen
(字节).传统的叫法是给一个套接字分配一个名字. 当使用 socket(2), 函数创
建一个套接字时,它存在于一个地址空间(地址族), 但还没有给它分配一个名字
sockfd:这是要绑定的套接字的文件描述符(socket file descriptor)。在创建套接字后,通过 socket
函数获得。该文件描述符指示操作系统内核中的套接字实例。
my_addr:这是一个指向 sockaddr
结构的指针,用于指定本地地址信息。sockaddr
结构是一个通用的套接字地址结构,它的具体类型取决于套接字的地址族(AF_INET、AF_INET6 等)。通常,您需要使用类型转换将 sockaddr_in
或 sockaddr_in6
结构的指针强制转换为 sockaddr
结构的指针。
例如,对于 IPv4 地址,可以这样使用:
struct sockaddr_in my_addr;
// 设置 my_addr 中的相关字段
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_in));
对于 IPv6 地址,可以使用类似的方法:
struct sockaddr_in6 my_addr;
// 设置 my_addr 中的相关字段
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_in6));
addrlen:这是 my_addr
结构的长度,以字节为单位。对于 IPv4 地址,应该是 sizeof(struct sockaddr_in)
,对于 IPv6 地址,应该是 sizeof(struct sockaddr_in6)
。
使用步骤如下:
创建套接字,获取套接字文件描述符 sockfd
,例如使用 socket
函数。
设置 my_addr
结构中的字段,以指定要绑定的本地地址信息。确保 sin_family
或 sin6_family
等字段设置为正确的地址族。
调用 bind
函数,将套接字 sockfd
与本地地址 my_addr
绑定。
检查 bind
函数的返回值,如果返回 -1
表示绑定失败,可以通过 perror
函数输出错误信息。
如果 bind
成功,套接字就与指定的本地地址绑定,其他套接字可以通过该地址与该套接字进行通信。
示例(IPv4 地址):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
// 创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置本地地址
struct sockaddr_in my_addr;
memset(&my_addr, 0, sizeof(struct sockaddr_in));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8080); // 设置端口号
my_addr.sin_addr.s_addr = INADDR_ANY; // 表示接受任意本地地址
// 绑定套接字与本地地址
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_in)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
// 绑定成功后,可以进行其他操作,比如监听连接、接受连接等
// 关闭套接字
close(sockfd);
return 0;
}
struct sockaddr_in
是用于表示 IPv4 地址的结构体。它定义在头文件 <netinet/in.h>
中。
以下是 struct sockaddr_in
结构体的详细解析:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* Internet address */
char sin_zero[8];/* padding to make structure the same size as sockaddr */
};
每个字段的解释如下:
sin_family:
sa_family_t
AF_INET
。sin_port:
in_port_t
htons()
函数将主机字节顺序转换为网络字节顺序。sin_addr:
类型:struct in_addr
描述:一个结构体,用于存储 IPv4 地址。其具体定义如下:
struct in_addr {
in_addr_t s_addr; /* IPv4 address in network byte order */
};
其中,s_addr
是一个 uint32_t
类型的值,表示 IPv4 地址。
sin_zero:
char[8]
struct sockaddr_in
结构体的大小与通用的 struct sockaddr
结构体大小相同而存在的填充字段。在某些早期的实现中,struct sockaddr
的大小是 16 字节,而 struct sockaddr_in
的大小是 16 字节,所以 sin_zero
被用来填充,以确保两者大小相同。但在现代的实现中,struct sockaddr
的大小通常已经被扩展到 28 字节或更多,所以这个填充字段不再是必需的。当你创建一个 struct sockaddr_in
实例并填充其字段时,你通常会为 sin_family
赋值为 AF_INET
,为 sin_port
赋值为你希望使用的端口号,并为 sin_addr.s_addr
赋值为你希望绑定的 IPv4 地址。
man 7 ip
NAME (名称)
ip - Linux IPv4 协议实现
SYNOPSIS(总览)
#include <sys/socket.h>
#include <net/netinet.h>
tcp_socket = socket(PF_INET, SOCK_STREAM, 0);
raw_socket = socket(PF_INET, SOCK_RAW, protocol);
udp_socket = socket(PF_INET, SOCK_DGRAM, protocol);
创建 TCP 套接字:
tcp_socket = socket(PF_INET, SOCK_STREAM, 0);
PF_INET
:这是协议族(Protocol Family)的标识符,表示 IPv4。SOCK_STREAM
:这是套接字类型,表示一个面向连接的、可靠的数据流套接字(即 TCP 套接字)。0
:这是套接字使用的具体协议。在此情况下,它会自动选择默认的协议,即 TCP。创建 RAW 套接字:
raw_socket = socket(PF_INET, SOCK_RAW, protocol);
SOCK_RAW
:这是另一种套接字类型,允许您访问协议的原始数据。创建这种套接字需要特定的权限,并且在大多数情况下,您需要以 root 用户身份运行程序。protocol
:这是一个特定于套接字类型的参数。对于 RAW 套接字,您需要指定希望捕获或发送的 IP 协议类型(如 ICMP、IGMP 等)。创建 UDP 套接字:
udp_socket = socket(PF_INET, SOCK_DGRAM, protocol);
SOCK_DGRAM
:这是套接字类型,表示一个无连接的、不可靠的数据报套接字(即 UDP 套接字)。网络字节顺序(通常称为大端字节顺序
)是计算机网络中用于数据传输的标准字节顺序。在大端字节顺序中,最高有效字节(Most Significant Byte,MSB)存储在最低的地址,而最低有效字节(Least Significant Byte,LSB)存储在最高的地址。
如果您的系统是小端字节顺序(MSB 存储在最高的地址,LSB 存储在最低的地址),而您希望与其他大多数网络设备进行通信(它们通常使用大端字节顺序),则需要使用如 htonl()
和 htons()
这样的函数来转换字节顺序。
man htonl
NAME
htonl, htons, ntohl, ntohs - convert values between host and network
byte order
SYNOPSIS
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);//h版本端口小端模式转网络字节序
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);//n版本网络字节序转端口小端
DESCRIPTION
The htonl() function converts the unsigned integer hostlong from host
byte order to network byte order.
下面是这些函数的简单描述:
htonl()
: host to network long
。此函数将一个无符号整数从主机字节顺序转换为网络字节顺序。
htons()
: host to network short
。此函数将一个无符号短整数(通常是端口号)从主机字节顺序转换为网络字节顺序。
ntohl()
: network to host long
。此函数将一个无符号整数从网络字节顺序转换回主机字节顺序。
ntohs()
: network to host short
。此函数将一个无符号短整数从网络字节顺序转换回主机字节顺序。
为了确保数据在网络上正确传输,当发送数据之前,通常需要使用 htonl()
或 htons()
转换数据;当接收数据时,通常需要使用 ntohl()
或 ntohs()
进行转换。
例如,如果您想将一个短整数(例如一个端口号)从小端字节顺序转换为网络字节顺序,您可以这样做:
uint16_t port = 12345; // 一个示例端口号
uint16_t network_port = htons(port);
这样,network_port
就是 port
的网络字节顺序表示形式。
这些函数是用于IPv4地址的转换和操作的。
man inet
NAME
inet_aton, inet_addr, inet_network, inet_ntoa, inet_makeaddr,
inet_lnaof, inet_netof - Internet address manipulation routines
SYNOPSIS
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);//将得到的IP地址转换成点分式的
struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);
in_addr_t inet_lnaof(struct in_addr in);
下面是对这些函数的简要描述:
inet_aton()
:
struct in_addr
结构体中。cp
为点分十进制的IPv4地址字符串,inp
是一个指向struct in_addr
的指针。1
,否则返回0
。inet_addr()
:
inet_aton()
类似,但是返回的是in_addr_t
类型的值。cp
为点分十进制的IPv4地址字符串。INADDR_NONE
。inet_network()
:
inet_addr()
类似,但是返回的是网络字节顺序的32位整数形式的IPv4网络地址(即网络号部分)。cp
为点分十进制的IPv4地址字符串。inet_ntoa()
:
in
是一个struct in_addr
结构体。inet_makeaddr()
:
net
为网络地址部分(网络字节顺序),host
为主机地址部分(网络字节顺序)。in_addr_t
值,代表组合的IPv4地址。inet_lnaof()
:
struct in_addr
结构体中获取主机地址部分。in
是一个struct in_addr
结构体。这些函数在进行网络编程时非常有用,因为它们允许开发者在不同的表示形式之间进行转换,从而使得网络通信更为方便。
accept
函数在UNIX和Linux系统中用于接受一个传入的连接请求。它常用于服务器端编程,特别是在使用TCP协议时。
man 2 accept
NAME 名称
accept - 在一个套接字上接收一个连接
SYNOPSIS 概述
#include <sys/types.h>
#include <sys/socket.h>
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
下面是对accept
函数的参数和使用的详细解释:
int s
:这是服务器监听套接字的描述符。当客户端尝试连接到服务器时,这个描述符将被用来接受连接。
struct sockaddr *addr
:这是一个指向struct sockaddr
的指针,用于存储客户端的地址信息。当accept
函数成功返回时,这个地址结构会被填充为客户端的地址。
socklen_t *addrlen
:这是一个指向socklen_t
类型的指针,指向addr
结构体的大小。当accept
函数成功返回时,这个指针的值会被更新为实际的addr
结构体的大小。
如果accept
成功接受了一个连接,它将返回一个新的套接字描述符,该描述符代表与客户端建立的连接。这个新的套接字可以用于与客户端进行数据交换。
如果accept
失败,它将返回-1
,并设置errno
以指示错误的原因。
connect
函数在UNIX和Linux系统中用于建立与远程服务器的连接。当客户端想要与服务器端的特定服务进行通信时,它会使用connect
函数。
man 2 connect
NAME
connect - initiate a connection on a socket
SYNOPSIS
#include <sys/types.h> /*See NOTES*/
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
以下是对connect
函数的参数和使用的详细解释:
int sockfd
:这是客户端的套接字描述符。该描述符应该是通过socket
函数创建的。
const struct sockaddr *addr
:这是一个指向struct sockaddr
类型的指针,它包含了服务器的地址信息。在使用TCP/IP时,这通常是一个指向struct sockaddr_in
类型的指针,其中包含了服务器的IP地址和端口号。
socklen_t addrlen
:这是一个表示addr
结构体的大小的值。
如果connect
函数成功建立了连接,它将返回0
。
如果connect
函数失败,它将返回-1
,并设置errno
以指示错误的原因。
man 2 recv
man 2 read
NAME
recv, recvfrom, recvmsg - receive a message from a socket
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssize_t read(int fd, void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
0
时,函数默认按照常规方式接收数据。recv
将返回-1
并设置errno
为EAGAIN
或EWOULDBLOCK
。ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
recv
。struct sockaddr
的指针,用于存储发送方的地址信息。socklen_t
的指针,用于指定src_addr
结构体的大小。这里的关键点是,当您使用recvfrom
从UDP套接字接收数据时,您不仅接收数据,还会得到发送方的地址。因此,这对于无连接的UDP套接字非常有用。
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
recvmsg
提供了一个更加灵活的接口,允许接收来自套接字的多个数据片段(或消息)。它使用一个包含多个缓冲区的struct msghdr
来处理这些数据。这在某些高级应用程序或特定的通信场景中可能很有用。
ssize_t read(int fd, void *buf, size_t count);
recv: 用于TCP套接字。它从连接的套接字上接收数据。如果没有数据可用,它可能会阻塞,除非设置了非阻塞模式。
recvfrom: 用于UDP套接字。除了接收数据,它还可以返回发送方的地址。这在UDP中很有用,因为UDP是无连接的。
recvmsg: 提供了更高级的功能,允许从一个套接字接收多个缓冲区的数据。这对于某些特定的应用程序和场景可能很有用。
read: 虽然它不是一个套接字特定的函数,但是它可以用于从任何文件描述符(包括套接字)中读取数据。
TCP三个都可以使用,但是UDP是无连接的,因此只能使用recivefrom。
man 2 send
man 2 write
NAME
send, sendto, sendmsg - 从套接字发送消息
概述
#include <sys/types.h>
#include <sys/socket.h>
int send(int s, const void *msg, size_t len, int flags);
int sendto(int s, const void *msg, size_t len, int flags, const struct
sockaddr *to, socklen_t tolen);
int sendmsg(int s, const struct msghdr *msg, int flags);
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
当然可以。下面是对这些发送函数的详细解释:
int send(int s, const void *msg, size_t len, int flags);
0
时,函数默认按照常规方式发送数据。send
将返回-1
并设置errno
为EAGAIN
或EWOULDBLOCK
。int sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
send
函数。struct sockaddr
结构体指针,指明了数据应该发送到哪里。to
指向的struct sockaddr
结构体的大小。使用sendto
时,您可以指定数据的接收者地址,这对于UDP套接字或在多播或广播场景中发送数据很有用。
int sendmsg(int s, const struct msghdr *msg, int flags);
struct msghdr
结构体指针。send
函数。sendmsg
提供了一个更加灵活的接口,允许发送来自多个缓冲区的数据片段(或消息)。这对于某些高级应用程序或特定的通信场景可能很有用。
ssize_t write(int fd, const void *buf, size_t count);
虽然write
函数不是发送函数,但它与发送函数有相似的工作原理,因此值得一提。
write
函数用于将数据写入文件描述符,这可能是套接字、文件或其他类型的描述符。在网络编程中,它经常用于发送数据到套接字。
TCP 三个都可以使用,UDP只能使用sendto
man 2 shutdown
man 2 close
NAME
shutdown - shut down part of a full-duplex connection
close - 关闭一个文件描述符
SYNOPSIS
#include <sys/socket.h>
int shutdown(int sockfd, int how);
#include <unistd.h>
int close(int fd);
If how is SHUT_RD,
further receptions will be disallowed. If how is SHUT_WR, further
transmissions will be disallowed. If how is SHUT_RDWR, further recep‐
tions and transmissions will be disallowed.
int shutdown(int sockfd, int how);
recv
或read
)将被禁止,但发送操作仍然可以进行。send
或write
)将被禁止,但接收操作仍然可以进行。这个函数通常在网络编程中用于优雅地关闭一个连接,尤其是当您想确保所有的数据都已经被接收或发送后再关闭连接时。
int close(int fd);
close
函数用于关闭文件描述符。在网络编程中,它经常用于关闭套接字描述符,从而终止与另一端的连接。
当使用close
关闭套接字描述符时,任何未发送或未接收的数据都将被丢弃,这可能导致连接重置(RST)消息被发送给另一端,这与shutdown
不同,shutdown
允许您更加优雅地关闭连接。