目录
核心大图:
网络字节序
网络字节序与主机字节序
地址转换函数
一、inet_ntoa函数
二、inet_aton函数
三、inet_aton和inet_ntoa的测试
in_addr转字符串的函数:
socket编程接口
socket 常见API
1.socket
参数1:int af
参数2:int type
参数3:int protocol
socket()函数返回值介绍
总结:
2.bind
1.函数的作用:
2.函数的声明:
3.端口问题
总结:
3.listen
1.函数原型
2. listen的第二个参数
4.accept
1. 函数原型
2. 参数SOCKET s:
3.返回值
4.accept特点
总结:
5.connect函数
1.函数原型:
2.函数参数:
3.函数功能:
总结
sockaddr结构
其他UDP函数
recvfrom函数
sendto()函数
udp小程序:单词替换检索程序
tcp小程序:单词替换检索程序(多进程和多线程版)
TCP协议通讯流程(很重要!!核心)
核心大图:
TCP
UDP
网络字节序
网络字节序与主机字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回 ;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
地址转换函数
//等会儿把编程接口看完再看这个
一、inet_ntoa函数
inet_ntoa函数所在的头文件:<arpa/inet.h>
函数原型:char *inet_ntoa(struct in_addr in);
函数功能:将一个网络字节序的IP地址(也就是结构体in_addr类型变量)转化为点分十进制的IP地址(字符串)。
函数形参in_addr
struct in_addr { in_addr_t s_addr; }; // in_addr是一个按网络顺序存储的IP地址。
函数返回值
该函数的返回值是一个字符串,这个字符串是点分十进制的IP地址。
二、inet_aton函数
inet_aton函数所在的头文件:<arpa/inet.h>
函数原型:int inet_aton(const char *IP, struct in_addr *addr);
函数功能:将一个字符串表示的点分十进制IP地址IP转换为网络字节序存储在addr中,并且返回该网络字节序表示的无符号整数。
函数形参
(1)const char *IP:我们输入的点分十进制的IP地址;
(2)struct in_addr* addr: 将IP转换为网络字节序(大端存储)后并保存在addr中;
函数返回值
失败:返回0;
成功:返回点分十进制的IP地址对应的网络字节序表示的无符号整数。
三、inet_aton和inet_ntoa的测试
#include<arpa/inet.h>
#include<stdlib.h>
#include<iostream>
int main()
{char IP[] = "159.12.8.109";in_addr address;int number = inet_aton(IP, &address);//将点分十进制的IP地址转化为二进制的网络字节序if(number == 0){std::cerr<<"error IP!";exit(1);}std::cout << number << std::endl;std::cout << inet_ntoa(address) << std::endl;//将网络字节序地址转化为点分十进制表示形式return 0;
}
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.
那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
运行结果如下:
因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.
思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
在APUE中, 明确提出inet_ntoa不是线程安全的函数;
但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
同学们课后自己写程序验证一下在自己的机器上inet_ntoa是否会出现多线程的问题;
在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问
题;
in_addr转字符串的函数:
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void
*addrptr。
#include <arpe/inet.h>
int inet_pton(int family, const char *strptr, void *addrptr); //将点分十进制的ip地址转化为用于网络传输的数值格式返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len); //将数值格式转化为点分十进制的ip地址格式返回值:若成功则为指向结构的指针,若出错则为NULL
(1)这两个函数的family参数既可以是AF_INET(ipv4)也可以是AF_INET6(ipv6)。如果,以不被支持的地址族作为family参数,这两个函数都返回一个错误,并将errno置为EAFNOSUPPORT.
(2)第一个函数尝试转换由strptr指针所指向的字符串,并通过addrptr指针存放二进制结果,若成功则返回值为1,否则如果所指定的family而言输入字符串不是有效的表达式格式,那么返回值为0.
(3)inet_ntop进行相反的转换,从数值格式(addrptr)转换到表达式(strptr)。inet_ntop函数的strptr参数不可以是一个空指针。调用者必须为目标存储单元分配内存并指定其大小,调用成功时,这个指针就是该函数的返回值。len参数是目标存储单元的大小,以免该函数溢出其调用者的缓冲区。如果len太小,不足以容纳表达式结果,那么返回一个空指针,并置为errno为ENOSPC。
4.示例
inet_pton(AF_INET, ip, &foo.sin_addr); // 代替 foo.sin_addr.addr=inet_addr(ip);char str[INET_ADDRSTRLEN];
char *ptr = inet_ntop(AF_INET,&foo.sin_addr, str, sizeof(str)); // 代替 ptr = inet_ntoa(foo.sin_addr)
in_addr_t inet_addr(const char *cp);
inet_addr函数转换网络主机地址(如192.168.1.10)为网络字节序二进制值,如果参数char *cp无效,函数返回-1(INADDR_NONE),这个函数在处理地址为255.255.255.255时也返回-1,255.255.255.255是一个有效的地址,不过inet_addr无法处理;
socket编程接口
socket 常见API
1.socket
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
参数1:int af
地址族规范。 地址族的可能值在Winsock2.h头文件中定义。
在为Windows Vista和更高版本发布的Windows SDK上,头文件的组织已更改,并且地址族的可能值在Ws2def.h头文件中定义。 请注意,Ws2def.h头文件自动包含在Winsock2.h中,并且绝对不能直接使用。
当前支持的值为AF_INET或AF_INET6,这是IPv4和IPv6的Internet地址族格式。 如果安装了地址族的Windows套接字服务提供程序,则支持地址族的其他选项(例如,与NetBIOS一起使用的AF_NETBIOS)。 请注意,AF_地址族和PF_协议族常量的值是相同的(例如,AF_INET和PF_INET),因此可以使用任何一个常量。
下表列出了地址系列的常用值,尽管许多其他值也是可能的
Af | Meaning |
---|---|
AF_UNSPEC 0 | 地址族未指定。 |
AF_INET 2 | Internet协议版本4(IPv4)地址族。 |
AF_IPX 6 | IPX / SPX地址族。 仅当安装了NWLink IPX / SPX NetBIOS兼容传输协议时,才支持此地址系列。WindowsVista和更高版本不支持此地址系列。 |
AF_APPLETALK 16 | AppleTalk地址族。 仅当安装了AppleTalk协议时才支持此地址系列。WindowsVista和更高版本不支持此地址系列。 |
AF_NETBIOS 17 | NetBIOS地址族。 仅当安装了用于NetBIOS的Windows套接字提供程序时,才支持此地址系列。在32位版本的Windows上支持用于NetBIOS的Windows套接字提供程序。 默认情况下,此提供程序安装在32位版本的Windows上.NetBIOS的Windows套接字提供程序在64位版本的Windows(包括Windows 7,Windows Server 2008,Windows Vista,Windows Server 2003或Windows XP)上不受支持。 用于NetBIOS的Windows套接字提供程序仅支持将* type 参数设置为* SOCK_DGRAM **的套接字。用于NetBIOS的Windows套接字提供程序与[NetBIOS]不直接相关(https://docs.microsoft.com/en -us / previous-versions / windows / desktop / netbios / portal)编程界面。 Windows Vista,Windows Server 2008和更高版本不支持NetBIOS编程接口。 |
AF_INET6 23 | Internet协议版本6(IPv6)地址族。 |
AF_IRDA 26 | 红外数据协会(IrDA)地址族。仅当计算机安装了红外端口和驱动程序时,才支持此地址族。 |
AF_BTH 32 | 蓝牙地址系列。如果计算机安装了蓝牙适配器和驱动程序,则Windows XP SP2或更高版本支持此地址系列。 |
参数2:int type
新套接字的类型规范。
套接字类型的可能值在Winsock2.h头文件中定义。
下表列出了Windows套接字2支持的type参数的可能值:
Type | Meaning | |
---|---|---|
SOCK_STREAM | 一种套接字类型,可通过OOB数据传输机制提供顺序的,可靠的,双向的,基于连接的字节流。 此套接字类型将传输控制协议(TCP)用于Internet地址系列(AF_INET或AF_INET6)。 | |
SOCK_DGRAM | 一种支持数据报的套接字类型,这些数据报是无连接的,不可靠的最大长度固定(通常很小)的缓冲区。 此套接字类型对Internet地址系列(AF_INET或AF_INET6)使用用户数据报协议(UDP)。 | |
SOCK_RAW |
| |
SOCK_RDM | 提供可靠消息数据报的套接字类型。 这种类型的一个示例是Windows中的实用通用多播(PGM)多播协议实现,通常称为可靠多播编程。仅当安装了可靠多播协议时才支持此* type *值。 | |
SOCK_SEQPACKET | 一种套接字类型,可根据数据报提供伪流数据包。 |
在Windows套接字2中,引入了新的套接字类型。 应用程序可以通过WSAEnumProtocols函数动态发现每个可用传输协议的属性。 因此,应用程序可以确定地址族的可能套接字类型和协议选项,并在指定此参数时使用此信息。 随着新套接字类型,地址族和协议的定义,Winsock2.h和Ws2def.h头文件中的套接字类型定义将定期更新。
参数3:int protocol
要使用的协议。 protocol参数的可能选项特定于指定的地址族和套接字类型。 在Winsock2.h和Wsrm.h头文件中定义了协议的可能值。
在为Windows Vista和更高版本发布的Windows SDK上,头文件的组织已更改,并且此参数可以是Ws2def.h头文件中定义的IPPROTO枚举类型的值之一。 请注意,Ws2def.h头文件自动包含在Winsock2.h中,并且绝对不能直接使用。
如果指定的值为0,则调用者不希望指定协议,服务提供商将选择要使用的协议。
当af参数为AF_INET或AF_INET6且类型为SOCK_RAW时,在IPv6或IPv4数据包头的协议字段中设置为协议指定的值。
- 通常情况下,如果某种协议类型只对应一种特定的协议,那么
protocol
参数可以设置为0,系统会自动选择该类型的默认协议。- 然而,如果某种协议类型支持多种协议,那么开发者就需要通过
protocol
参数来明确指定所使用的协议。例如,在使用原始套接字(SOCK_RAW
)时,可能需要指定具体的IP协议(如IPPROTO_TCP
、IPPROTO_UDP
等)。
IPPROTO_TCP
:表示传输控制协议(TCP),是一种面向连接的、可靠的、基于字节流的传输层通信协议。IPPROTO_UDP
:表示用户数据报协议(UDP),是一种无连接的、不可靠的、基于数据报的传输层通信协议。IPPROTO_SCTP
:表示流控制传输协议(SCTP),是一种面向消息的、可靠的、多流的传输层通信协议。IPPROTO_TIPC
:表示透明进程间通信协议(TIPC),是一种用于集群内部进程间通信的协议。
socket()函数返回值介绍
SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (INVALID_SOCKET == socketServer)
{int a = WSAGetLastError();printf("socket function failed with error = %d\n", a);//清理网络库WSACleanup();return 1;
}closesocket(socketServer);
一、成功返回值
当socket函数调用成功时,它会返回一个非负整数,这个整数就是新创建的套接字的描述符。在Unix-like系统中,文件描述符是一个用于标识打开文件的整数,而套接字作为一种特殊的文件类型,同样使用文件描述符进行标识。
- 文件描述符的特性:
- 文件描述符是一个非负整数。
- 每个进程都有自己独立的文件描述符空间。
- 文件描述符在进程的生命周期内有效,进程终止后文件描述符随之失效。
- 套接字描述符的作用:
- 套接字描述符用于在后续的网络编程操作中标识该套接字。
- 可以通过套接字描述符进行绑定地址和端口、监听连接请求、接受连接、发送和接收数据等操作。
二:失败返回值
如果socket函数调用失败,它会返回-1,并设置全局变量errno以指示具体的错误原因。errno是一个在头文件<errno.h>中定义的整数变量,用于存储最近一次系统调用或库函数调用的错误代码。
- 常见的socket错误代码:
- EINVAL:无效参数。传递给socket函数的参数不合法。
- EMFILE:进程文件描述符表已满。当前进程已经打开了太多的文件或套接字。
- ENOBUFS或ENOMEM:系统内存不足。系统没有足够的内存来分配新的套接字。
- EACCES:权限被拒绝。当前用户没有足够的权限来创建套接字。
- EAFNOSUPPORT:地址族不被支持。指定的协议族在当前系统上不被支持。
- EPROTOTYPE:协议类型不被支持。指定的套接字类型或协议在当前系统上不被支持。
- 错误处理:
- 当socket函数调用失败时,应首先检查errno的值以了解具体的错误原因。
- 根据错误原因进行相应的错误处理,如释放已分配的资源、记录日志、向用户显示错误信息等。
总结:
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
应用程序可以像读写文件一样用read/write在网络上收发数据;
如果socket()调用出错则返回-1;
对于IPv4, family参数指定为AF_INET;
对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
protocol参数的介绍从略,指定为0即可。
2.bind
1.函数的作用:
服务端用于将把用于通信的地址和端口绑定到 socket 上。所以可以猜出,这个函数的参数应该包含:用于通信的 socket 和服务端的 IP 地址和端口号。ip地址和端口号是放在 socketaddr_in 结构体里面的。
2.函数的声明:
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
1)参数 sockfd ,需要绑定的socket。
(2)参数 addr ,存放了服务端用于通信的地址和端口。
(3)参数 addrlen ,表示 addr 结构体的大小
(4)返回值:成功则返回0 ,失败返回-1,错误原因存于 errno 中。如果绑定的地址错误,或者端口已被占用,bind 函数一定会报错,否则一般不会返回错误。
3.端口问题
1.端口用了一次,还没有释放再用这个端口,出现被占用的情况。
服务端 socket 的 SO_REUSEADDE 属性
1.服务端程序的端口释放后可能会处于 TIME_WAIT 状态(等待),要等待两分钟后才能被再次使用,解决方法:设置 SO_REUSEADDE 选项,让端口释放后立即可以被再次使用。
2.设置 SO_REUSEADDE 选项,把这段代码写入服务端程序。
int opt = 1; unsigned int len = sizeof(opt);
setsockopt(listenfd,SOL_SOCKET,REUSEADDR,&opt,len);
总结:
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;
bind()成功返回0,失败返回-1。
bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号;
前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度;
我们的程序中对myaddr参数是这样初始化的:
1. 将整个结构体清零;
2. 设置地址类型为AF_INET;
3. 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
4. 端口号为SERV_PORT, 我们定义为9999;
3.listen
1.函数原型
int WSAAPI listen(SOCKET s,int backlog
);
功能:listen函数会把sockfd的套接字标记为被动的监听listen状态,之后服务端与客户端通信的整个流程中sockfd只有listen和closed两种状态,第二个参数backlog代表等待队列的最大长度。
参数:
SOCKET s:
- 服务端的socket,也就是socket函数创建的
- 标识绑定的,未连接的套接字的描述符。
int backlog:
挂起的连接队列的最大长度。
就是说,比如有100个用户链接请求,但是系统一次只能处理20个,那么剩下的80个不能不理人家,所以系统就创建个队列记录这些暂时不能处理,一会儿处理的连接请求,依先后顺序处理,那这个队列到底多大?就是这个参数设置,比如2,那么就允许两个新链接排队。这个不能无限大,那内存就不够了。
我们可以手动设置这个参数,但是别太大了。可能2-10多,~20多。
我们一般填写这个参数为SOMAXCONN
作用是让系统自动选择最合适的个数
不同的系统环境不一样,所以这个合适的数也不一样
2. listen的第二个参数
最大队列个数是backlog+1。
客户端:
当connect到来的时候无论等待队列有没有空余地方在客户端眼里都是连接成功的,因为一开始调用connect函数SYN请求,服务端立刻给予客户端SYN+ACK确认,客户端连接状态从SYN_SENT转换到ESTABLISHED之后再向服务端发ACK确认。也就是所谓的三次握手过程
服务端:
当客户端connect来到时,服务端进入SYN_RCVD状态并给予SYN+ACK响应。当下次客户端完成三次握手,收到客户端的ACK时:如果队列中有空间,则服务端的连接也建立成功,否则服务端眼里没有成功。每建立成功一次,要往队列中放入刚才建立好的连接,也就是队列空间-1,服务端状态从SYN_RCVD变为ESTABLISHED,如果不成功还是原来的SYN_RCVD状态。当服务端调用accept成功时,又是队列空间+1,从队列中拿走一个连接。
这里我在网上找了一张大体流程图方便大家理解:
三次握手过程:
4.accept
1. 函数原型
accept函数允许在套接字上进行传入连接尝试
SOCKET WSAAPI accept(SOCKET s,sockaddr *addr,int *addrlen
);
listen监听客户端来的链接,accept将客户端的信息绑定到一个socket上,也就是给客户端创建一个socket,通过返回值返回给我们客户端的socket。
一次只能创建一个,有几个客户端链接,就要调用几次。
2. 参数
SOCKET s:
一个描述符,用于标识已使用侦听功能置于侦听状态的套接字。 实际上,连接是通过accept返回的套接字建立的。
*sockaddr addr:
通信层已知的指向接收连接实体地址的缓冲区的可选指针。 addr参数的确切格式由创建sockaddr结构的套接字时建立的地址族确定。
*int addrlen:
指向整数的可选指针,该整数包含addr参数指向的结构的长度。
3.返回值
成功:
- 返回值就是给客户端包好的socket
- 与客户端通信就靠这个
失败:
- 返回
INVALIE_SOCKET
- 通过
WSAGetLastError()
得到错误码
4.accept特点
- 阻塞、同步:这个函数是阻塞的,没有客户端连接,那就一直卡在这儿等着。
- 多个链接:一次只能一个,5个就要5次循环
总结:
三次握手完成后, 服务器调用accept()接受连接;
如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
如果给addr 参数传NULL,表示不关心客户端的地址;
addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
5.connect函数
1.函数原型:
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
2.函数参数:
参数说明:
sockfd:fd
addr:用来指定服务端的addr地址信息,对这个addr发起连接
addrlen:addr的长度
返回值:成功则返回一个新的fd,这个fd用来和对端进行通信;失败则返回-1,并且设置errno;
3.函数功能:
发起连接server操作,在其内部会进行3次握手触发,3次握手成功就会返回一个fd
总结
客户端需要调用connect()连接服务器;
connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
connect()成功返回0,出错返回-1;
sockaddr结构
从网络编程的API中我们看到一个需要强转的结构体(struct sockaddr),为什么要强转成这个结构体呢?
先说结论,sockaddr是统一的接口,只用一个接口完成不同套接字(比如IPV4,IPV6)之间的通信问题。
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同。在C语言中如果直接处理就要多出重复的接口,设计成统一的目的是为了设计尽量少的接口,实现面向对象中的静态多态——函数重载。
共同点是三者的首地址都是存的地址类型。
这16位地址类型是什么呢?不妨看看sockaddr_in的源码。其中的16位端口号一目了然。
并且搜索之下找到了struct的in_addr的说明,是32位的IP的地址
那最后开头的__kernel_sa_family_t是什么东西呢?就是一个short类型的数字,用来标识是什么类型,是IPV4还是IPV6。
因此,所有的接口都是struct sockaddr
类型,如何区分对于具体的传入使用哪个结构体。提取第一个字段进行if
判断,如果为AF_INET
则为struct sockaddr_in
。而且每个结构的大小不同,所以还需要传入用户层实际结构体的长度进行处理。
如此一来就达到同一套接口,传入参数的不同,进行不同的函数操作,达到了静态多态——函数重载。
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结构体指针做为参数;
其他UDP函数
recvfrom函数
recvfrom()
函数是一个系统调用,用于从套接字接收数据。该函数通常与无连接的数据报服务(如 UDP)一起使用,但也可以与其他类型的套接字使用。与简单的 recv()
函数不同,recvfrom()
可以返回数据来源的地址信息。
函数原型为:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
参数解释:
sockfd:一个已打开的套接字的描述符。
buf:一个指针,指向用于存放接收到的数据的缓冲区。
len:缓冲区的大小(以字节为单位)。
flags:控制接收行为的标志。通常可以设置为0,但以下是一些可用的标志:
MSG_WAITALL:尝试接收全部请求的数据。函数可能会阻塞,直到收到所有数据。
MSG_PEEK:查看即将接收的数据,但不从套接字缓冲区中删除它【1】。
其他一些标志还可以影响函数的行为,但在大多数常规应用中很少使用。
src_addr:一个指针,指向一个 sockaddr 结构,用于保存发送数据的源地址。
addrlen:一个值-结果参数。开始时,它应该设置为 src_addr 缓冲区的大小。当 recvfrom() 返回时,该值会被修改为实际地址的长度(以字节为单位)。
返回值:
在成功的情况下,recvfrom() 返回接收到的字节数。
如果没有数据可读或套接字已经关闭,那么返回值为0。
出错时,返回 -1,并设置全局变量 errno 以指示错误类型。
例子:
struct sockaddr_in sender;
socklen_t sender_len = sizeof(sender);
char buffer[1024];int bytes_received = recvfrom(sockfd, buffer, sizeof(buffer), 0,(struct sockaddr*)&sender, &sender_len);
if (bytes_received < 0) {perror("recvfrom failed");// handle error
}
在这个例子中,我们使用 recvfrom()
从套接字 sockfd
接收数据。发送者的地址和端口信息保存在 sender
结构中。
sendto()函数
sendto()
函数是一个系统调用,用于发送数据到一个指定的地址。它经常与无连接的数据报协议,如UDP,一起使用。不像 send()
函数只能发送数据到一个预先建立连接的远端,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:在数据报协议下告诉网络层该数据已经被确认。
MSG_DONTROUTE:不查找路由,数据报将只发送到本地网络。
其他标志可以影响函数的行为,但在大多数常规应用中很少使用。
dest_addr:指向 sockaddr 结构的指针,该结构包含目标地址和端口信息。
addrlen:dest_addr 缓冲区的大小(以字节为单位)。
返回值:
成功时,sendto() 返回实际发送的字节数。
出错时,返回 -1 并设置全局变量 errno 以指示错误类型。
例子:
struct sockaddr_in receiver;
receiver.sin_family = AF_INET;
receiver.sin_port = htons(12345); // Some port number
inet_pton(AF_INET, "192.168.1.1", &receiver.sin_addr); // Some IP addresschar message[] = "Hello, World!";
ssize_t bytes_sent = sendto(sockfd, message, sizeof(message), 0,(struct sockaddr*)&receiver, sizeof(receiver));
if (bytes_sent < 0) {perror("sendto failed");// handle error
}
在这个例子中,我们使用 sendto()
发送一个字符串到指定的IP地址和端口号。如果发送失败,我们打印一个错误消息。
recvfrom
函数之所以能够在接收到数据时获取到发送方的地址信息,是因为UDP(用户数据报协议)是一种无连接的协议,每个数据报都是独立发送和接收的。与TCP不同,UDP不维护连接状态,因此每个数据报都必须携带足够的信息来让接收方知道它是从哪里发送来的。当您调用
sendto
函数发送数据时,您必须指定目标地址(即接收方的IP地址和端口号)。同样地,当数据报在网络上传输时,它也会携带发送方的地址信息(通常是源IP地址和源端口号),这样接收方就能够知道数据是从哪里发送来的。当
recvfrom
函数被调用以接收数据时,它实际上是在等待一个到达指定套接字的数据报。一旦数据报到达,recvfrom
会做以下几件事情:
- 从套接字缓冲区中读取数据报的内容。
- 检查数据报的头部信息,以获取发送方的地址信息(源IP地址和源端口号)。
- 将数据报的内容复制到调用者提供的缓冲区中。
- 如果调用者提供了一个有效的
sockaddr
结构体指针和相应的长度指针,recvfrom
会将发送方的地址信息复制到这个结构体中,并更新长度。因此,当您调用
recvfrom
并传入一个指向sockaddr
结构体的指针时,这个结构体在函数返回后将被填充为发送方的地址信息。这样,您就可以通过检查这个结构体来获取发送方的IP地址和端口号。这个过程是UDP协议的一部分,它确保了即使在没有建立连接的情况下,接收方也能够知道每个数据报的来源。
udp小程序:单词替换检索程序
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段