网络编程
网络协议
TCP/UDP对比#
-
TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前 不需要建立连接
-
TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
-
TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
-
每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
-
TCP首部开销20字节;UDP的首部开销小,只有8个字节
-
TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
端口号作用#
一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等
这些服务完全可以通过1个IP地址来实现。那么,主机是怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP 地址与网络服务的关系是一对多的关系。
实际上是通过“IP地址+端口号”来区分不同的服务的。
端口提供了一种访问通道,
服务器一般都是通过知名端口号来识别的。例如,对于每个TCP/IP实现来说,FTP服务器的TCP端口号都是21,每个Telnet服务器的TCP端口号都是23,每个TFTP(简单文件传送协议)服务器的UDP端口号都是69。
字节序:
1. Little endian:将低序字节存储在起始地址
2. Big endian:将高序字节存储在起始地址
网络字节序=大端字节序
在网络编程中,字节序(Byte Order)指的是多个字节数据在内存中存储的顺序。由于不同的计算机体系结构和操作系统采用的字节序不同,因此在进行网络通信时,需要统一字节序,以确保数据的正确传输和解析。
常见的字节序有两种:大端序(Big Endian)和小端序(Little Endian)。
大端序是指高位字节存储在内存的低地址处,低位字节存储在内存的高地址处。例如,十六进制数0x12345678在大端序中存储的顺序为0x12 0x34 0x56 0x78。
小端序则是相反的,它是指低位字节存储在内存的低地址处,高位字节存储在内存的高地址处。例如,十六进制数0x12345678在小端序中存储的顺序为0x78 0x56 0x34 0x12。
在网络编程中,通常采用大端序进行数据传输,这是因为大端序是网络协议中规定的标准字节序。例如,在TCP/IP协议中,IP地址和端口号都采用大端序进行传输。
在实际编程中,可以使用一些函数来进行字节序的转换。例如,在C语言中,可以使用htons()和htonl()函数将主机字节序转换为网络字节序,也可以使用ntohs()和ntohl()函数将网络字节序转换为主机字节序。其中,htons()和ntohs()函数用于16位整数的转换,而htonl()和ntohl()函数用于32位整数的转换。
例如,将一个16位整数从主机字节序转换为网络字节序,可以使用htons()函数:
uint16_t host_int = 0x1234;
uint16_t net_int = htons(host_int);
将一个32位整数从网络字节序转换为主机字节序,可以使用ntohl()函数:
uint32_t net_int = 0x12345678;
uint32_t host_int = ntohl(net_int);
需要注意的是,在进行字节序转换时,需要确保数据类型的大小和字节序的匹配。例如,如果使用htons()函数将一个32位整数转换为网络字节序,可能会导致数据截断或错误的字节序。因此,在进行字节序转换时,需要根据数据类型选择正确的函数。
另外,需要注意的是,不同的编程语言和操作系统可能对字节序的处理方式不同。因此,在进行跨平台的网络编程时,需要特别注意字节序的处理,以确保数据的正确传输和解析。
总之,字节序在网络编程中是一个非常重要的概念,它涉及到数据的正确传输和解析。了解字节序的概念和处理方式,可以帮助开发者编写更加健壮
字节序转换api#
htons()
和ntohs()
函数:这些函数用于将16位整数从主机字节序转换为网络字节序和从网络字节序转换为主机字节序。在<arpa/inet.h>
头文件中定义。
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);
uint16_t ntohs(uint16_t netshort);
htonl()
和ntohl()
函数:这些函数用于将32位整数从主机字节序转换为网络字节序和从网络字节序转换为主机字节序。在<arpa/inet.h>
头文件中定义。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint32_t ntohl(uint32_t netlong);
h代表host,n代码net,s代码short(两个字节),l代表long(4个字节),通过上面的4个函数可以实现主机字节序和网络字节序之间的转换。有时可以用INADDR_ANY,INADDR_ANY指定地址让操作系统自己获取.用于端口
地址转换API#
inet_aton#
用于将IPv4地址的字符串表示转换为32位二进制网络字节序的整数。它的函数原型如下:
int inet_aton(const char *cp, struct in_addr *inp);
函数的参数说明如下:
- straddr:表示要转换的IPv4地址的字符串表示,例如"192.168.1.1"。
- addrp:表示一个指向in_addr结构体的指针,用于存储转换后的二进制数。
函数的返回值为一个整型数,如果转换成功返回1,否则返回0。
例如,对于IPv4地址字符串"192.168.0.1",inet_aton函数将返回一个32位整数0xc0a80200001,其中0xc0对应十进制数192,0xa8对应十进制数168,0x00对应十进制数0,0x01对应十进制数1。
需要注意的是,inet_aton函数返回的32位整数是网络字节序(big-endian)的,即高位字节在前,低位字节在后。如果需要在主机字节序(little-endian或big-endian,取决于主机架构)和网络字节序之间进行转换,可以使用htonl和ntohl函数。
例如,将一个IPv4地址字符串转换为网络字节序的32位整数的示例代码如下:
#include <stdio.h>
#include <arpa/inet.h>
int main()
{
const char *straddr = "192.168.1.1";
struct in_addr addr;
if (inet_aton(straddr, &addr))
{
printf("转换成功,二进制数为:%u\n", addr.s_addr);
}
}
如果转换成功,inet_aton函数会将转换后的二进制数存储在addrp指向的in_addr结构体中,并返回1。如果转换失败,函数返回0。
in_addr结构体定义如下:
struct in_addr {
in_addr_t s_addr; // 32位IPv4地址
};
其中,in_addr_t是一个无符号整型数,用于存储32位IPv4地址的二进制数。
在上面的示例代码中,我们将字符串"192.168.1.1"转换为二进制数,并将结果存储在addr结构体中。我们可以通过访问addr结构体的s_addr成员来获取转换后的二进制数。
inet_ntoa 和 inet_ntop#
用于将一个IPv4地址从二进制格式转换为点分十进制格式。
char *inet_ntoa(struct in_addr inaddr);
函数的参数是一个in_addr结构体类型的变量inaddr,它包含了一个IPv4地址的二进制表示。函数返回一个指向表示该IPv4地址的点分十进制字符串的指针。
例如,如果inaddr的值为0x01020304,那么inet_ntoa函数将返回一个指向字符串"1.2.3.4"的指针。
需要注意的是,该函数返回的指针指向的是静态内存,因此多次调用该函数将会覆盖之前的结果。如果需要保存多个IPv4地址的点分十进制表示,应该将结果复制到另一个缓冲区中另外需要注意的是,该函数存在线程安全问题。由于返回的指针指向的是静态内存,因此在多线程环境下,多个线程可能会同时调用该函数并修改同一块静态内存,导致结果不可预测。
为了避免这个问题,可以使用另一个函数inet_ntop()来代替inet_ntoa()。inet_ntop()函数也可以将IPv4地址从二进制格式转换为点分十进制格式,但它是线程安全的,并且可以处理IPv4和IPv6地址。
inet_ntop()函数的原型如下:
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
其中,af参数指定地址族,可以是AF_INET表示IPv4地址,也可以是AF_INET6表示IPv6地址。src参数是一个指向存储地址的缓冲区的指针,dst参数是一个指向存储点分十进制表示的缓冲区的指针,size参数指定了dst缓冲区的大小。
例如,如果要将一个IPv4地址转换为点分十进制格式,可以使用以下代码:
#include <arpa/inet.h>
#include <stdio.h>
int main()
{
struct in_addr addr;
addr.s_addr = htonl(0x01020304);
char buf[INET_ADDRSTRLEN];
const char *str = inet_ntop(AF_INET, &addr, buf, INET_ADDRSTRLEN);
if (str == NULL)
{
perror("inet_ntop");
return 1;
}
printf("IPv4 address: %s\n", str);
return 0;
}
该程序将输出"IPv4 address: 1.2.3.4"。
需要注意的是,inet_ntop()函数的返回值是一个指向点分十进制表示的缓冲区的指针,因此不需要担心线程安全问题。另外,由于该函数可以处理IPv6地址,因此在编写网络程序时建议使用inet_ntop()函数来替代inet_ntoa()函数。
最后,需要注意的是,由于IPv4地址和IPv6地址的长度不同,因此在使用inet_ntop()函数时需要根据地址族的不同来指定缓冲区的大小。对于IPv4地址,可以使用INET_ADDRSTRLEN常量来指定缓冲区的大小,该常量定义在<arpa/inet.h>头文件中。对于IPv6地址,可以使用INET6_ADDRSTRLEN常量来指定缓冲区的大小。
Sockt服务器和客户端的开发步骤
-
创建套接字
-
为套接字添加信息(ip地址和端口号)
-
监听网络连接
-
监听到有客户端接收,接收一个连接
-
数据交互
-
关闭套接字,断开连接
我是服务器
是说汉语(TCP/UDP)
我ip地址(楼号)是 。。
我端口号(房间号)是。。
我在监听(等待大家的来访,来了敲门)
1、socket(创建套接字)#
使用socket函数创建套接字(socket)并进行网络通信。
int socket(int domain, int type, int protocol);
domain
:#
指明所使用的协议族,通常为AF_INET
,表示互联网协议族(TCP/IP协议族):
AF_INET
IPv4因特网域AF_INET6
IPv6因特网域AF_UNIX
Unix域AF_ROUTE
路由套接字AF_KEY
密钥套接亨AF_UNSPEC
未指定
type
参数指定socket
的类型:#
- SOCK_STREAM
SOCK_STREAM是一种套接字类型,它提供了一个可靠的、面向连接的服务,使用TCP协议进行通信。这种套接字类型提供了一个字节流接口,数据按顺序传输,不会出现数据重复或丢失的情况。在使用SOCK_STREAM类型的套接字时,需要先建立连接,然后才能进行数据传输。这种套接字类型适用于需要可靠传输的应用程序,如文件传输、电子邮件、Web浏览器等。
- SOCK_DGRAM
SOCK_DGRAM一种无连接的套接字类型,它使用UDP协议进行数据传输。与面向连接的TCP套接字不同,使用SOCK_DGRAM发送数据时不需要先建立连接,而是直接将数据发送到目标地址。由于SOCK_DGRAM是无连接的,因此它不会对数据进行可靠性保证,也不会保证数据的顺序。这意味着在使用SOCK_DGRAM发送数据时,可能会出现数据丢失、重复、乱序等问题。因此,SOCK_DGRAM适用于一些对数据可靠性要求不高的场景,例如音视频传输、实时游戏等。
- SOCK_RAW
允许程序使用底层协议,原始套接字允许对底层协议如IP或ICMP进行直接访问,功能强大但使用较为不便,主要用于一些协议的开发。SOCK_RAW是一种套接字类型,它允许应用程序直接访问网络层协议,如IP协议。使用SOCK_RAW套接字,应用程序可以发送和接收原始的网络数据包,而不需要经过传输层协议(如TCP或UDP)的处理。
SOCK_RAW套接字通常用于网络诊断和安全应用程序,如网络嗅探器、防火墙和入侵检测系统。但是,使用SOCK_RAW套接字需要特权访问,因为它可以绕过传输层协议的安全机制,可能会对网络安全造成威胁。
protocol
#
通常赋值0
- 0选择
type
类型对应的默认协议 IPPROTO_TCP
TCP传输协议IPPROTO_UDP
UDP传前协议IPPROTO_SCTP
SCTP传输协议IPPROTO_TIPC
TIPC传输协议
2、地址准备好#
bind()函数#
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能#
- 用于绑定IP地址和端口号到socketfd
参数#
- sockfd
是一个socket描述符
- addr
是一个指向包含有本机IP
地址及端口号等信息的sockaddr
类型的指针,指向要绑定给sockfd
的协议地址结构,这个地址结构根据地址创建socket
时的地址协议族的不同而不同。通常使用的是sockaddr结构体的一种变体,如sockaddr_in、sockaddr_in6等。
struct sockaddr
{
sa_family_t sa_family; // 地址族,如AF_INET、AF_INET6等
char sa_data[14]; // 地址数据,具体含义取决于地址族
};
重点
在实际使用中,通常使用sockaddr的一种变体,如sockaddr_in、sockaddr_in6等,这些结构体包含了更具体的地址信息。
struct sockaddr_in {
sa_family_t sin_family; // 地址族,固定为AF_INET
in_port_t sin_port; // 端口号,使用htons()函数转换为网络字节序
struct in_addr sin_addr;// IP地址,使用inet_aton()或inet_addr()函数转换为网络字节序
char sin_zero[8]; // 保留字段,必须为0
};
在调用bind()函数时,需要将要绑定的地址结构体的指针强制转换为sockaddr结构体的指针类型,例如:
struct sockaddr_in addr;
// 初始化addr结构体
// ...
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
其中,(struct sockaddr *)&addr是将addr结构体的指针强制转换为sockaddr结构体的指针类型,以便传递给bind()函数。
最后,addrlen参数是地址结构体的长度,通常使用sizeof()函数来获取。例如:
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
其中,sizeof(addr)是addr结构体的长度,即要绑定的地址结构体的长度。
需要注意的是,addr参数的具体类型和内容取决于要绑定的地址族和协议类型。例如,如果要绑定的地址族是AF_INET(IPv4),则addr参数应该是一个指向sockaddr_in结构体的指针,其中包含了要绑定的IPv4地址和端口号等信息。如果要绑定的协议类型是SOCK_STREAM(TCP),则需要使用socket()函数创建一个TCP套接字,然后将其绑定到指定的地址上。
3、监听#
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能#
- 设置能处理的最大连接数,
listen()
并未开始接受连线,只是设置sockect
的listen
模式,listen
函数只用于服务器端,服务器进程不知道要与谁连接,因此,它不会主动地要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后响应该连接请求,并对它做出处理,一个服务进程可以同时处理多个客户进程的连接。主要就两个功能:将一个未连接的套接字转换为一个被动套接字(监听),规定内核为相应套接字排队的最大连接数。 - 内核为任何一个给定监听套接字维护两个队列:
- 未完成连接队列,每个这样的
SYN
报文段对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的TCP三次握手过程。这些套接字处于SYN_REVD
状态: - 已完成连接队列,每个已完成TCP三次握手过程的客户端对应其中一项。这些套接字处于
ESTABLISHED
状态
- 未完成连接队列,每个这样的
参数#
-
sockfd
sockfd
参数是要监听的套接字的文件描述符。该套接字必须是一个已经通过socket()
函数创建的套接字,并且已经通过bind()
函数绑定到一个本地地址上。 -
backlog
backlog
参数是指定等待连接队列的最大长度。当有新的连接请求到达时,如果等待连接队列已满,则新的连接请求将被拒绝。backlog
参数的具体含义因操作系统而异,但通常它指定了内核为该套接字维护的已完成连接队列的最大长度。
返回值#
listen()
函数成功返回0,失败返回-1,并设置errno变量以指示错误类型。常见的错误类型包括:
-
EBADF
:sockfd
不是有效的文件描述符。 -
EINVAL
:sockfd
不是一个监听套接字,或backlog
参数小于0。 -
ENOTSOCK
:sockfd
不是一个套接字。 -
EOPNOTSUPP
:该套接字类型不支持监听操作。 -
EADDRINUSE
:该套接字已经绑定到一个地址上,并且该地址已经被其他套接字使用。 -
EFAULT
:backlog
参数指向的内存地址不可访问。
4、连接#
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept()
函数用于接受一个已经建立的连接请求,并创建一个新的套接字来与客户端进行通信。
功能#
- accept函数由
TCP
服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。
参数#
-
sockfd
:监听套接字的文件描述符。该套接字必须是已经通过listen()
函数标记为被动套接字的套接字。 -
addr
:一个指向sockaddr
结构体的指针,用于存储客户端的地址信息。在函数返回时,addr
结构体将被填充为客户端的地址信息,包括IP地址和端口号。如果不需要获取客户端的地址信息,可以将该参数设置为NULL
。 -
addrlen
:一个指向socklen_t
类型变量的指针,用于存储addr
结构体的长度。在函数调用之前,需要将该变量设置为addr
结构体的长度。在函数返回时,该变量将被设置为实际的addr
结构体长度。
返回值#
accept()
函数成功返回一个新的套接字文件描述符,该套接字用于与客户端进行通信。如果函数调用失败,则返回-1,并设置errno变量以指示错误类型。常见的错误类型包括:
EBADF
:sockfd
不是有效的文件描述符。ECONNABORTED
:连接被终止。EFAULT
:addr
或addrlen
参数指向的内存地址不可访问。EINVAL
:sockfd
不是一个监听套接字。ENFILE
或EMFILE
:系统中已经打开的文件描述符数量达到了上限。ENOBUFS
或ENOMEM
:系统内存不足,无法创建新的套接字。ENOTSOCK
:sockfd
不是一个套接字。
需要注意的是,accept()
函数是一个阻塞函数,它会一直等待直到有新的连接请求到达。如果没有新的连接请求到达,该函数将一直阻塞。如果需要非阻塞地等待连接请求,可以使用select()
或poll()
等函数。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周明内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示TCP三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。
5、数据收发#
接受数据函数#
recv()函数是套接字通信中用于接收数据的函数
int recv(int sockfd, void *buf, size_t len, int flags);
-
sockfd:套接字描述符,用于标识一个已连接的套接字。
-
buf:指向接收数据的缓冲区。
-
len:缓冲区的大小,即最多可以接收的数据量。
-
flags:接收数据的选项,可以为0或以下常量的按位或:
-
MSG_WAITALL
: 等待接收到指定大小的数据后才返回。如果接收到的数据小于指定大小,则recv()
函数将一直等待,直到接收到指定大小的数据或者出现错误。 -
MSG_PEEK
: 接收数据时不将数据从接收缓冲区中删除,而是将数据复制到缓冲区中,并返回数据的长度。这个标志通常用于查看接收缓冲区中的数据,而不是真正地接收数据。 -
MSG_OOB
: 接收带外数据。带外数据是指在TCP连接中,发送方可以通过发送紧急数据的方式来传递一些重要的信息,接收方可以通过MSG_OOB
标志来接收这些带外数据。 -
MSG_DONTWAIT
: 非阻塞模式。如果没有数据可接收,则recv()
函数将立即返回,并返回一个错误码EAGAIN
或EWOULDBLOCK
。 -
MSG_CMSG_CLOEXEC
: 在接收到控制消息时,将FD_CLOEXEC
标志设置为控制消息中包含的文件描述符。这个标志通常用于在接收控制消息时自动关闭文件描述符,以避免文件描述符泄漏的问题。 -
MSG_TRUNC
: 如果接收到的数据比缓冲区大小大,则数据会被截断,并返回截断后的数据长度。这些常量可以通过按位或运算符(|)组合使用,例如:flags = MSG_WAITALL | MSG_PEEK。
-
返回值为接收到的数据的字节数,如果出现错误则返回-1。
recv()函数是一个阻塞函数,即如果没有数据可接收,它将一直等待直到有数据到达。如果需要在接收数据时设置超时时间,可以使用select()函数来实现。需要注意的是,recv()
函数只能用于已连接的套接字,如果要接收未连接的套接字的数据,需要使用recvfrom()
函数。另外,recv()
函数只能接收TCP协议的数据,如果要接收UDP协议的数据,需要使用recvfrom()
函数。recv()函数的flags参数也可以设置为0,这表示没有特殊的标志位被设置。在这种情况下,recv()函数将以默认的方式接收数据。默认情况下,recv()函数将以阻塞模式接收数据,即如果没有数据可读,则函数将一直阻塞,直到有数据可读或发生错误为止。
除了recv()
函数之外,还可以使用C语言标准库中的read()
和write()
函数进行套接字通信。
read()
函数:从文件描述符读取数据。
ssize_t read(int fd, void *buf, size_t count);
其中,fd
参数是文件描述符,可以是套接字的文件描述符,buf
参数是指向接收缓冲区的指针,count
参数是接收缓冲区的长度。
read()
函数成功返回接收到的字节数,失败返回-1
2.write()
函数:向文件描述符写入数据。
ssize_t write(int fd, const void *buf, size_t count);
其中,fd
参数是文件描述符,可以是套接字的文件描述符,buf
参数是指向发送缓冲区的指针,count
参数是发送缓冲区的长度。
write()
函数成功返回发送的字节数,失败返回-1
发送数据函数#
send()函数是套接字发送数据的函数
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
send()函数的参数说明如下:
-
sockfd:表示要发送数据的套接字描述符。
-
buf:表示要发送的数据缓冲区的地址。
-
len:表示要发送的数据的长度。
-
flags:表示发送数据时的标志位,可以设置为0或以下常量的按位或:
当flags参数为0时,表示发送操作使用默认的标志。默认情况下,send()函数将数据发送到已连接的套接字,如果数据长度超过套接字缓冲区的大小,则会阻塞直到缓冲区有足够的空间可供使用。如果套接字已设置为非阻塞模式,则send()函数会立即返回,并设置errno变量为EAGAIN或EWOULDBLOCK,表示发送操作会阻塞。
在大多数情况下,使用默认的标志是安全和可靠的,因为它可以保证数据的可靠传输,并且不会导致网络拥塞或安全漏洞。但是,在某些特殊情况下,可能需要使用其他标志来控制发送操作的行为,例如发送带外数据或请求对方确认接收到数据。在这种情况下,需要根据实际需求选择合适的标志,并确保正确使用以避免出现错误。
-
MSG_DONTWAIT:表示在发送数据时不阻塞进程,如果发送缓冲区已满,则立即返回-1并设置errno为EAGAIN或EWOULDBLOCK。
-
MSG_NOSIGNAL:表示在发送数据时不产生SIGPIPE信号。SIGPIPE信号通常在套接字已关闭或连接已断开时产生,如果进程没有处理该信号,则会导致进程终止。使用MSG_NOSIGNAL标志可以避免这种情况的发生
-
MSG_OOB:用于发送带外数据。带外数据是指在正常数据流之外发送的数据,通常用于紧急情况。带外数据可以通过SO_OOBINLINE套接字选项启用或禁用。
-
MSG_WAITALL:用于阻塞直到所有数据都被接收或者出现错误。如果接收方没有接收到所有数据,则send()函数会一直阻塞,直到接收到所有数据或者出现错误。
-
MSG_CONFIRM:用于请求对方确认接收到数据。当发送方发送数据时,如果设置了MSG_CONFIRM标志,则接收方必须发送一个确认消息,以便发送方知道数据已经被接收。如果接收方没有发送确认消息,则发送方会重试发送数据,直到达到最大重试次数或出现错误。
-
MSG_DONTROUTE标志用于指示内核不应该查找路由表来确定数据的下一跳地址,而是直接将数据发送到目标地址。这通常用于本地网络中的数据传输,因为在这种情况下,数据包不需要经过路由器。
-
MSG_NOSIGNAL标志用于指示在发送数据时不产生SIGPIPE信号。SIGPIPE信号通常在套接字已关闭或连接已断开时产生,如果进程没有处理该信号,则会导致进程终止。使用MSG_NOSIGNAL标志可以避免这种情况的发生。
-
MSG_MORE标志表示还有更多数据要发送,内核会将数据缓存起来,直到后续的send()调用。这通常用于发送大量数据时,可以将数据分成多个块发送,以避免一次性发送过多数据导致网络拥塞或内存不足。
-
MSG_EOR标志表示当前发送的数据是一个消息的结尾。这通常用于发送多个消息时,可以将每个消息分成多个块发送,以便接收方可以区分每个消息的边界。
使用flags参数时需要谨慎,因为不正确的使用可能会导致数据丢失、网络拥塞或安全漏洞。
-
客户端
connect#
connect函数用于建立与远程服务器的连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd:已经创建好的套接字描述符,可以通过socket函数创建得到。
- addr:指向远程服务器地址结构体的指针,可以是sockaddr_in或sockaddr_in6类型的结构体指针。
- addrlen:addr结构体的大小,可以通过sizeof函数获取。
对于addr结构体,它的具体定义取决于使用的协议族。在IPv4协议族中,通常使用sockaddr_in结构体,它的定义如下:
struct sockaddr_in {
sa_family_t sin_family; // 地址族,一般为AF_INET
in_port_t sin_port; // 端口号,使用网络字节序
struct in_addr sin_addr; // IP地址
char sin_zero[8]; // 未使用
如果连接成功,connect函数返回0;如果连接失败,返回-1,并设置errno变量来指示错误类型。可以使用perror函数来输出错误信息。
编程实战
server.c#
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
//#include <linux/in.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv)
{
int s_fd;
int c_fd;
int n_read;
char readBuf[128];
int mark = 0;
char msg[128] = {0};
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
if(argc != 3){
printf("param is not good\n");
exit(-1);
}
memset(&s_addr,0,sizeof(struct sockaddr_in));
memset(&c_addr,0,sizeof(struct sockaddr_in));
//1. socket
s_fd = socket(AF_INET, SOCK_STREAM, 0);
if(s_fd == -1){
perror("socket");
exit(-1);
}
s_addr.sin_family = AF_INET;
//atoi() 函数将字符串转换为整数。htons() 函数用于将主机字节序转换为网络字节序,确保在不同的系统中传输时端口号的字节顺序是正确的。
s_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&s_addr.sin_addr);//将ip地址转化为网络字节序
//2. bind
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
//3. listen
listen(s_fd,10);
//4. accept
int clen = sizeof(struct sockaddr_in);
while(1){
c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);
if(c_fd == -1){
perror("accept");
}
printf("get connect: %s\n",inet_ntoa(c_addr.sin_addr));
mark++;
if(fork() == 0){
if(fork()==0){
while(1){
memset(msg,0,sizeof(msg));
printf("input%d:\n ",mark);
gets(msg);
if(strcmp(msg,"quit") == 0){
usleep(1000);
continue;
}
write(c_fd,msg,strlen(msg));
}
}
//5. read
while(1){
memset(readBuf,0,sizeof(readBuf));
n_read = read(c_fd, readBuf, 128);
if(n_read == -1){
perror("read");
}else{
printf("\nget%d: %s\n",mark,readBuf);
}
}
break;
}
}
return 0;
}
client.c#
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char const *argv[])
{
int c_fd;
int n_read;
char readBuf[128];
char msg[128] = {0};
struct sockaddr_in c_addr;
memset(&c_addr, 0, sizeof(struct sockaddr_in));
// 1.socket
c_fd = socket(AF_INET, SOCK_STREAM, 0);
if (c_fd == -1)
{
perror("socket");
exit(-1);
}
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1], &c_addr.sin_addr);
// 2.connect
int flg = connect(c_fd, (struct sockaddr *)&c_addr, sizeof(struct sockaddr));
if (flg == -1)
{
perror("connect");
exit(-1);
}
while (1)
{
if (fork() == 0)
{
while (1)
{
memset(msg, 0, sizeof(msg));
printf("input: ");
gets(msg);
write(c_fd, msg, strlen(msg));
}
}
while (1)
{
memset(readBuf, 0, sizeof(readBuf));
n_read = read(c_fd, readBuf, 128);
if (n_read == -1)
{
perror("read");
}
else
{
printf("get message from server:%d,%s\n", n_read, readBuf);
}
}
}
return 0;
}
服务端不能实现给多个客户端发送信息,调用子进程后并不清楚会把那个信息发送给哪个客户端,改进后的代码可以在每个用户提示用户是第几个连接进来的。
serverpro.c#
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
// #include <linux/in.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
// chenlichen
int main(int argc, char **argv)
{
int s_fd;
int c_fd;
int n_read;
char readBuf[128];
int mark = 0;
char msg[128] = {0};
// char *msg = "I get your connect";
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
if (argc != 3)
{
printf("param is not good\n");
exit(-1);
}
memset(&s_addr, 0, sizeof(struct sockaddr_in));
memset(&c_addr, 0, sizeof(struct sockaddr_in));
// 1. socket
s_fd = socket(AF_INET, SOCK_STREAM, 0);
if (s_fd == -1)
{
perror("socket");
exit(-1);
}
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1], &s_addr.sin_addr);
// 2. bind
bind(s_fd, (struct sockaddr *)&s_addr, sizeof(struct sockaddr_in));
// 3. listen
listen(s_fd, 10);
// 4. accept
int clen = sizeof(struct sockaddr_in);
while (1)
{
c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &clen);
if (c_fd == -1)
{
perror("accept");
}
mark++;
printf("get connect: %s\n", inet_ntoa(c_addr.sin_addr));
if (fork() == 0)
{
if (fork() == 0)
{
while (1)
{
sprintf(msg, "welcom No.%d client", mark);
write(c_fd, msg, strlen(msg));
sleep(3);
}
}
// 5. read
while (1)
{
memset(readBuf, 0, sizeof(readBuf));
n_read = read(c_fd, readBuf, 128);
if (n_read == -1)
{
perror("read");
}
else if (n_read > 0)
{
printf("\nget: %d\n", n_read);
}
else
{
printf("client quit\n");
break;
}
}
break;
}
}
return 0;
}
作者:keep--fighting
出处:https://www.cnblogs.com/keep--fighting/p/17745509.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
· Manus的开源复刻OpenManus初探