3. Linux网络编程基础API

1.主机字节序和网络字节序
现代 CPU 的累加器一次都能装载(至少)4个字节(对于 32 位的机器),即一个 int 类型。那么这4个字节在内存中排列的顺序将影响它被累加器装载的整数值,这就是字节序问题。
字节存储顺序主要分为大端序(Big-endian)和小端序(Little-endian),区别如下
- Big-endian:高位字节存入低地址,低位字节存入高地址
- Little-endian:低位字节存入低地址,高位字节存入高地址
例如,将12345678h
写入1000h
开始的内存中,以大端序和小端序模式存放结果如下:

现代 PC 大多采用小端字节序,因此小端字节序又称为主机字节序;
而在两台不同字节序的主机之间传递数据时,发送端总是把要发送的数据转化成大端字节序数据后再发送,因此大端字节序也被称为网络字节序。
网络字节顺序
是TCP / IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式。
“大同小异 ”:大端字节序的 字节顺序 和 内存地址顺序相同;小端字节序则相反。
2.通用 socket 地址和专用 socket 地址
socket 网络编程接口中表示 socket 地址是结构体 sockaddr,其定义如下:
1 2 3 4 5 6 7 8
| #include <bits/socket.h> struct sockaddr{ sa_family_t sa_family; char sa_data[14]; }; typedef unsigned short int sa_family_t;
|
sa_family
成员是地址族类型(sa_family_t
)的变量。地址族类型通常与协议类型对应。常见的协议族和对应的地址族如下所示:

而sa_data
成员用于存放socket地址值。但是,不同的协议族的地址值具有不同的含义和长度 :

可以看到,14个字节只能装下 IPv4地址,没办法装下IPv6的地址。因此,该结构体表示方式已经被废掉,Linux定义了下面这个新的通用的socket地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的(内存对齐可以加快CPU访问速度)
1 2 3 4 5 6 7 8
| #include <bits/socket.h> struct sockaddr_storage { sa_family_t sa_family; unsigned long int __ss_align; char __ss_padding[ 128 - sizeof(__ss_align) ]; }; typedef unsigned short int sa_family_t;
|
Linux 为各个协议族提供了专门的 socket 地址结构体。
UNIX 本地域协议族使用如下专用的 socket 地址结构体:
1 2 3 4 5 6
| #include <sys/un.h> struct sockaddr_un { sa_family_t sin_family; /* 地址族: AF_UNIX */ char sun_path[108]; /* 文件路径名 */ };
|
TCP / IP 协议族有 sockaddr_in
和 sockaddr_in6
两个专用的 socket 地址结构体,它们分别用于 IPv4 和 IPv6:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <netinet/in.h> struct sockaddr_in { sa_family_t sin_family; /* 地址族: AF_INET */ in_port_t sin_port; /* 端口号: 用网络字节序表示 */ struct in_addr sin_addr; /* IPv4地址结构体 */ }; struct in_addr { u_int32_t s_addr; /* IPv4地址, 用网络字节序表示 */ }; struct sockaddr_in6 { sa_family_t sin6_family; /* 地址族: AF_INET6 */ in_port_t sin6_port; /* 端口号: 用网络字节序表示 */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 地址结构体 */ uint32_t sin6_scope_id; /* IPv6 scope-id */ }; struct in6_addr { unsigned char sa_addr[16]; /* IPv6 地址, 用网络字节序表示 */ };
|
所有专用 socket 地址(以及 sockaddr_storage
)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr
(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。
3.IP 地址转换函数
人们习惯用可读性好的字符串来表示IP地址,比如用点分十进制字符串表示IPV4地址,以及用十六进制字符串表示IPv6地址,但编程中我们需要先把他们转化为整数(二进制)方能使用。而记录日志相反,我们需要把整数表示的IP地址转化为可读的字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| /* #include <arpa/inet.h> // p:点分十进制的IP字符串,n:表示network,网络字节序的整数 int inet_pton(int af, const char *src, void *dst); af: 地址族: AF_INET AF_INET6 src: 需要转换的点分十进制的IP字符串 dst: 转换后的结果保存在这个里面 // 将网络字节序的整数,转换成点分十进制的IP地址字符串 const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); af: 地址族: AF_INET AF_INET6 src: 要转换的ip的整数的地址 dst: 转换成IP地址字符串保存的地方 size:第三个参数的大小(数组的大小) 返回值:返回转换后的数据的地址(字符串),和 dst 是一样的 */ #include <stdio.h> #include <arpa/inet.h> int main() { // 创建一个ip字符串, 点分十进制的IP地址字符串 char buf[] = "192.168.1.0"; unsigned int num = 0; // 将 点分十进制的IP字符串 转换成 网络字节序的整数 inet_pton(AF_INET, buf, &num); unsigned char *p = (unsigned char *)# printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3)); // 将 网络字节序的IP整数 转换成 点分十进制的IP字符串 char ip[16] = ""; //字符串 IP 地址四段,每段最多三个字节,加上3个“.”,再加一个字符串结束符 const char * str = inet_ntop(AF_INET, &num, ip, 16); printf("str : %s\n", str); printf("ip : %s\n", ip); printf("%d\n", ip == str); return 0; }
|

一个 IPv4 地址,如果用字符串数组来存储,最多需要多大的字符串数组?
IPv4 地址格式:
- IPv4 地址是由四个八位字节(每个0-255之间)组成,每个字节以
.
分隔。
- 一个完整的 IPv4 地址的格式示例为
255.255.255.255
,这个是最大可能的字符串长度。
字符数计算:
- 每个字节(0-255)可以用 1 到 3 个字符表示(如
0
, 10
, 255
)。
- 四个字节之间有三个
.
分隔符。
- 因此,最大长度的 IPv4 地址字符串是
xxx.xxx.xxx.xxx
,总共 15 个字符。
字符串结束符:
- C 语言中的字符串以
\0
结束符结尾,所以需要额外的 1 个字节空间来存储这个结束符。
因此,字符串数组大小应该为 16 ,才可以存储最完整的 IPv4 地址字符串形式(15个字符 xxx.xxx.xxx.xxx
)和一个结束符(\0
)。
4.创建 socket
1 2 3 4 5
| #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> int socket(int domain,int type,int protoco1);
|
**功能:**创建一个套接字
参数:
domain
:底层协议族
- PF_INET:IPv4
- PF_INET6:IPv6
- PF_UNIX:UNIX 本地域协议族
type
:指定通信过程中使用的服务类型
SOCK_STREAM
:流服务(TCP等)
SOCK_DGRAM
:数据报服务(UDP等)
- 自 Linux 内核版本 2.6.17 起,
type
参数可以接受上述服务类型与下面两个重要的标志相与的值:SOCK_NONBLOCK
和 SOCK_CLOEXEC
,前者将 socket 设置为非阻塞的。
protocol
:在前两个参数构成的协议集合下,再选择一个具体的协议。一般设置为 0(因为前面两个参数已经完全决定了它的值)
返回值:
成功:返回 socket 文件描述符,操作的就是内核缓冲区
失败:返回-1 并设置 errno
5.命名 socket
即将一个 socket 与 socket 地址绑定。
1
| int bind(int sockfd,const struct sockaddr* my_addr,socklen_t addrlen);
|
**功能:**将 my_addr
所指的 socket 地址分配给未命名的 sockfd
文件描述符,addrlen
指出 socket 地址长度。
参数:
sockfd
:通过socket
函数得到的文件描述符
my_addr
:需要绑定的 socket 地址,这个地址封装了ip 地址和端口号的信息
addrlen
:第二个参数结构体占的内存大小
返回值:
成功:返回 0
失败:返回-1 并设置 errno
6.监听 socket
socket 被命名后,还不能马上接受客户连接,需要使用如下系统调用,创建一个监听队列,存放待处理的客户连接:
1
| int listen(int sockfd,int backlog);
|
**功能:**监听指定 socket上的连接
参数:
sockfd
:通过socket()
函数得到的文件描述符,指定被监听的 socket
backlog
:提示内核监听队列的最大长度,超过该设定的最大值,服务器将不受理新的客户连接。
返回值:
成功:返回 0
失败:返回 -1 并设置 errno
就相当于创建了一个监听套接字,监听套接字的文件描述符是sockfd,如果客户端连接监听套接字,那么由accept创建一个新的套接字用于和客户端连接,而监听套接字继续监听
而在客户端也只是知道监听套接字里面的ip和端口,而不知道accept新创建的套接字的ip和端口号

7.接受连接
1
| int accept(int sockfd,struct sockaddr *addr ,sock1en_t *addrlen);
|
**功能:**从 listen 监听队列中接受一个连接。accept 只是从监听队列中取出连接,而不关心连接处于何种状态。
参数:
sockfd
:执行过 listen
系统调用的监听 socket
addr
:记录了连接成功后客户端的地址信息(socket 地址)
addrlen
:指定第二个参数的 socket 地址长度。
返回值:
成功:返回一个新的连接 socket,该 socket 唯一标识了被接受的这个连接,服务器可以通过读写该 socket 来与被接受连接的客户端通信。
失败:返回-1 并设置 errno
8.发起连接
服务器通过 listen
调用来被动接受连接,而客户端通过 connect
来主动与服务器发起连接。
1
| int connect(int sockfd,const struct sockaddr *serv_addr,socklen_t addr1en);
|
参数:
sockfd
:用于通信的文件描述符,由 socket
系统调用返回。
addr
:客户端要连接的服务器的地址信息,即服务器监听的 socket 地址。
addrlen
:addr
的地址长度。
返回值:
成功:返回0,连接成功建立,则 sockfd
唯一标识这个连接,客户端可以通过读写 sockfd
来与服务器通信。
失败:返回 -1并设置 errno
9.关闭连接
**功能:**关闭该连接对应的 socket
参数:
fd
:待关闭的 socket。注意,close
调用并非总是立即关闭一个连接,而是将 fd
的引用计数减一。当引用计数为 0 时,才真正关闭这个连接。如果要立即终止连接,可以使用 shutdown
系统调用:
1
| int shutdown(int sockfd, int howto);
|
**功能:**立即关闭该连接对应的 socket
参数:
sockfd
:待关闭的 socket。
howto
:决定了 shutdown
的行为。
SHUT_RD
:断开输入流。套接字无法接收数据(即使输入缓冲区收到数据也被抹去),无法调用输入相关函数。
SHUT_WR
:断开输出流。套接字无法发送数据,但如果输出缓冲区中还有未传输的数据,则将传递到目标主机。
SHUT_RDWR
:同时断开 I/O 流。相当于分两次调用 shutdown()
,其中一次以 SHUT_RD
为参数,另一次以 SHUT_WR
为参数。
返回值:
成功:返回0
失败:返回 -1并设置 errno
shutdown
可以分别关闭 socket 上的读和写,而 close
只能同时关闭。
多进程中一次fork会让socket的引用计数+1,所以父子进程都要释放socket
10.数据读写
1.TCP 数据读写
用于 TCP 流数据的读写调用:
1 2 3 4 5
| #include <sys/types.h> #include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags) ssize_t send(int sockfd, const void *buf, size_t len, int flags)
|
recv()
成功时返回实际读取到的数据长度,可能小于指定读缓冲区的大小 len
,因此可能需要多次调用才能读取完整数据。返回 0 说明对方已经关闭连接,返回-1 说明出错。
send()
成功时返回实际写入 sockfd 的数据长度,失败返回-1
flags
参数为数据收发提供了额外的控制。只对 send
和 recv
的当前调用生效。而 setsockopt
可以永久修改 socket 的某些属性。
注意:
如果是:客户端给服务器端发送数据,服务器接受客户端数据
send发送的时候,参数sockfd是客户端自己的socket
recv接受的时候,参数sockfd是服务器端的accept创建的新的socket(即accept的返回值)
2.UDP 数据读写
系统调用是:
1 2 3 4 5 6 7
| #include <sys/types.h> #include <sys/socket.h> ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
|
recvfrom
:
参数:
sockfd
:一个已打开的套接字的描述符。
buf
:一个指针,指向读缓冲区。
len
:读缓冲区的大小(以字节为单位)。
src_addr
:一个指针,指向发送端的 socket 地址。(UDP 是无连接的,每次通信都需要指定通信地址)
addrlen
:开始时,它应该设置为 src_addr
缓冲区的大小。当 recvfrom()
返回时,该值会被修改为实际地址的长度(以字节为单位)。
flags
:控制接收行为的标志。通常可以设置为0。
返回值:
在成功的情况下,recvfrom()
返回接收到的字节数。
如果没有数据可读或 socket 已经关闭,那么返回值为0。
出错时,返回 -1,并设置全局变量 errno 以指示错误类型。
sendto
:与recvfrom
类似。
实际上,recvfrom
和sendto
也可以用于面向连接的 socket 的数据读写,当连接已经建立时,只需要把recvfrom
/sendto
的 src_addr
和addrlen
设置为 NULL
,即可忽略发送端/接收端的 socket 地址。
3.通用数据读写
不仅能用于 TCP 流数据,还能用于 UDP 数据报。
1 2 3 4
| #include <sys/socket.h> ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); ssize_t recvmsg(int sockfd, const struct msghdr *msg, int flags);
|
msghdr有个成员是msg_name,存储的是和本主机通信的socket的地址
对于udp这个就是对方的socket,对于tcp这个值没有意义,必须要设置为null
11.带外标记
Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据需要接收。内核通知应用程序带外数据到达的两种常见方式是:I/O复用产生的异常事件和SIGURG信号。但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。这一点可通过如下系统调用实现:
1 2
| #include <sys/socket.h> int sockatmark( int sockfd );
|
sockatmark
判断 sockfd
是否处于带外标记,即下一个被读取到的数据是否是带外数据。
如果是,sockatmark
返回1,此时我们就可以利用带MSG_OOB
标志的recv
调用来接收带外数据。如果不是,则sockatmark
返回0。
带外数据补充说明:
TCP 带外数据(Out-of-Band Data)是指在 TCP 连接中,可以优先处理的特殊数据流。这种机制允许一些数据在正常的 TCP 流中被打断和优先处理,从而在需要时可以在数据流中插入重要信息,如紧急控制信息或命令。
主要特点:
- 紧急指针 (Urgent Pointer): TCP 通过设置一个紧急指针来标识带外数据的结束位置。接收方在接收到带外数据时会注意到这个指针,并处理相应的数据。
- 优先级: 带外数据通常需要优先于其他正常数据进行处理。这对某些需要快速响应的应用非常重要,比如远程登录或其他需要交互的场景。
- 使用情境: 典型的使用场景包括需要快速中断当前传输的操作,例如在一个远程终端中发送一个中断命令,在 TCP 数据流的中断点(如发送 Ctrl+C 执行中解除)。
注意事项:
- 带外数据并不是绝对可靠的,尤其在某些网络状况下,带外数据可能会丢失。
- 带外数据的使用复杂且不够普遍,许多应用程序并不广泛使用这一功能。
- 由于不同操作系统间的实现可能不同,带外数据的处理可能会引起不兼容的问题。
实际使用:
尽管 TCP 提供了带外数据的机制,实际编码中很多开发者选择使用其他手段(如特定消息或信号)来实现通信中的紧急处理,主要是因为带外数据的复杂性和不一致性。在许多情况下,使用正常 TCP 流进行控制传输的方式可能更加简单和可靠。
12.地址信息函数
在某些情况下,我们想知道一个连接socket的本端socket地址,以及远端的socket地址。下面这两个函数正是用于解决这个问题:
1 2 3 4
| #include <sys/socket.h> int getsockname( int sockfd,struct sockaddr* address,socklen t* address_len ); int getpeername( int sockfd,struct sockaddr* address, socklen t* address_len );
|
getsockname
获取sockfd
对应的本端socket地址,并将其存储于address
参数指定的内存中,该socket地址的长度则存储于address_len
参数指向的变量中。如果实际socket地址的长度大于 address所指内存区的大小,那么该socket地址将被截断。getsockname
成功时返回0,失败返回-1并设置 errno。
getpeerame
获取 sockfd
对应的远端socket地址,其参数及返回值的含义与 getsockname
的参数及返回值相同。
13.socket 选项
类似fcntl设置文件描述符属性
用来读取和设置socket文件描述符属性的方法:
1 2 3 4 5 6
| #include <sys/socket.h> int getsockopt( int sockfd, int level,int option_name, void* option_value, socklen_t* restrict option_len ); int setsockopt( int sockfd, int level,int option_name, const void* option_value, socklen_t option_len );
|
常用:
- SO_REUSEADDR: 强制使用被处于 TIME_WAIT 状态的连接占用的 socket 地址
- SO_RCVBUF/SO_SNDBUF: 表示TCP 接收缓冲区和发送缓冲区的大小
- SO_RECLOWAT/SO_SNDLOWAT: 表示TCP 接收缓冲区和发送缓冲区的低水位标记,一般被I/O 复用系统调用用来判断socket是否可读或者可写(默认1字节)
- SO_LINGER: 控制close系统调用在关闭TCP连接时候的行为
14.网络信息 API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include<netdb.h>
struct hostent*gethostbyname(const char*name);
struct hostent*gethostbyaddr(const void*addr,size_t len,int type);
struct servent*getservbyname(const char*name,const char*proto);
struct servent*getservbyport(int port,const char*proto);
int getaddrinfo(const char*hostname,const char*service,const struct addrinfo*hints,struct addrinfo**result);
int getnameinfo(const struct sockaddr*sockaddr,socklen_t addrlen,char*host,socklen_t hostlen,char*serv,socklen_t servlen,int flags);
|
上述4个函数都是不可重入的,即非线程安全的。 给出了可重入版本
15.实战1:TCP通信实现(服务器端和客户端)
接下来,我们通过一个实战小项目,来更加深刻的理解 TCP 通信和 socket 的使用。项目代码来自:【Linux】socket 编程(socket套接字介绍、字节序、socket地址、IP地址转换函数、套接字函数、TCP通信实现)_linux socket-CSDN博客
整个流程经历了以下阶段:
- 在代码中,服务器端先通过
socket
调用,创建了一个 socket;
- 服务器端调用
bind
将 socket 绑定到一个 IP 地址和端口号;
- 服务器端调用
listen
使 socket 进入监听状态,准备接收客户端的连接请求;
- 客户端调用
connect
,发起连接请求;
- 服务器端调用
accept
,接受客户端的连接请求;
- TCP 连接建立,可以通过
read
/write
收发数据。
服务器端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| #include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include <stdlib.h> /* TCP服务器端实现 */ int main() { /* * 1.创建socket(用于监听的套接字): * 使用 socket 函数创建一个套接字 listen_fd * AF_INET: 表示使用 IPv4 协议 * SOCK_STREAM: 表示使用 TCP 连接。 */ int listen_fd = socket(AF_INET, SOCK_STREAM, 0); if(listen_fd == -1) { perror("socket"); exit(-1); } /* * 2.绑定/命名 socket: * struct sockaddr_in 用于指定服务器的地址和端口。 * INADDR_ANY 让服务器绑定到所有可用的网络接口(即可以接受任何 IP 地址的连接)。 * 使用 bind 函数将套接字与指定的 IP 地址和端口绑定。 * 如果 bind 返回 -1,表示绑定失败,输出错误信息并退出程序。 */ struct sockaddr_in saddr; saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0 saddr.sin_port = htons(9999); int ret = bind(listen_fd, (struct sockaddr *)&saddr, sizeof(saddr)); if(ret == -1) { perror("bind"); exit(-1); } /* * 3.监听连接 * listen 函数使套接字进入监听状态,准备接收来自客户端的连接请求。 * 第二个参数 8 指定了连接队列的最大长度,即最多可以有 8 个连接请求排队等待处理。 * 如果 listen 返回 -1,表示监听失败,输出错误信息并退出程序。 */ ret = listen(listen_fd, 8); if(ret == -1) { perror("listen"); exit(-1); } /* * 4.接收客户端连接 * 使用 accept 函数接受客户端的连接请求,accept 返回一个新的套接字 cfd,用于与客户端通信。 * clientaddr 用于存储客户端的地址信息。 * 如果 accept 返回 -1,表示接收连接失败,输出错误信息并退出程序。 */ struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr); int cfd = accept(listen_fd, (struct sockaddr *)&clientaddr, &len); if(cfd == -1) { perror("accept"); exit(-1); } /* * 5. 输出客户端的信息 * inet_ntop 函数将客户端的 IP 地址从网络字节序转换为点分十进制字符串格式,并输出。 * ntohs 函数将客户端的端口号从网络字节序转换为主机字节序,并输出。 */ char clientIP[16]; inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP)); unsigned short clientPort = ntohs(clientaddr.sin_port); printf("client ip is %s, port is %d\n", clientIP, clientPort); // 6.通信 char recvBuf[1024] = {0}; while(1) { // 获取客户端的数据 int num = read(cfd, recvBuf, sizeof(recvBuf)); if(num == -1) { perror("read"); exit(-1); } else if(num > 0) { printf("recv client data : %s\n", recvBuf); } else if(num == 0) { // 表示客户端断开连接 printf("clinet closed..."); break; } char *data = "hello,i am server"; // 给客户端发送数据 write(cfd, data, strlen(data)); } // 7. 关闭文件描述符 close(cfd); close(listen_fd); return 0; }
|
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| #include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include <stdlib.h> /* TCP客户端实现 */ int main() { // 1.创建套接字: AF_INET 表示使用 IPv4 地址,SOCK_STREAM 表示使用 TCP 连接。 int fd = socket(AF_INET, SOCK_STREAM, 0); if(fd == -1) { perror("socket"); exit(-1); } // 2.连接服务器端: 设定服务器的 IP 地址和端口号(此处为 "10.1.1.161" 和 9999)。 struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; inet_pton(AF_INET, "10.1.1.161", &serveraddr.sin_addr.s_addr); serveraddr.sin_port = htons(9999); int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); if(ret == -1) { perror("connect"); exit(-1); } // 3.通信 char recvBuf[1024] = {0}; while(1) { char *data = "hello,i am client"; // 通过 write 函数发送消息 "hello,i am client" 到服务器。 write(fd, data , strlen(data)); sleep(1); int len = read(fd, recvBuf, sizeof(recvBuf)); if(len == -1) { perror("read"); exit(-1); } else if(len > 0) { printf("recv server data : %s\n", recvBuf); } else if(len == 0) { // 如果 read 返回 0,表示服务器关闭了连接,客户端程序将结束循环并关闭连接。 printf("server closed..."); break; } } // 4. 关闭连接 close(fd); return 0; }
|
效果
编译运行上述代码,在终端中可以看单到:


16.实战 2:使用 MSG_OOB
选项发送带外数据

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <string.h> #include <unistd.h>
class TCPServerSocket{ private: struct sockaddr_in address; int sockfd; public: TCPServerSocket(const char *ip, int port){ sockfd = socket( PF_INET, SOCK_STREAM, 0 ); assert(sockfd >= 0); memset(&address, 0, sizeof(address)); address.sin_family = AF_INET; address.sin_port = htons(port); assert(inet_aton(ip, &address.sin_addr) == 1); } ~TCPServerSocket() { close(sockfd); } int bindSocket(); int listenSocket(int backlog); int acceptConnection(struct sockaddr_in* client, socklen_t* client_addrlength); int getSocketFd() const { return sockfd; } };
int TCPServerSocket::bindSocket(){ return bind(sockfd, (struct sockaddr*)&address, sizeof(address)); }
int TCPServerSocket::listenSocket(int backlog){ return listen(sockfd, backlog); }
int TCPServerSocket::acceptConnection(struct sockaddr_in* client, socklen_t* client_addrlength){ int newsockfd = accept(sockfd, (struct sockaddr*)client, client_addrlength); if (newsockfd < 0) { return -1; } return newsockfd; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <string.h> #include <unistd.h> #include <stdexcept> class TCPClientSocket { private: int sockfd; struct sockaddr_in server_addr; public: TCPClientSocket(const char* server_ip, int server_port) { sockfd = socket(PF_INET, SOCK_STREAM, 0); if (sockfd < 0) { throw std::runtime_error("Failed to create socket"); } memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(server_port); if (inet_aton(server_ip, &server_addr.sin_addr) == 0) { throw std::runtime_error("Invalid server IP address"); } } ~TCPClientSocket() { close(sockfd); } void connectToServer() { if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { throw std::runtime_error("Failed to connect to server"); } } int getSocketFd() const { return sockfd; } };
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include "TCPClientSocket.h" #include <iostream> #include <stdlib.h>
int main(int argc, char *argv[]){ if (argc <= 2){ std::cout << "usage: " << basename(argv[0]) << " ip_address port_number" << std::endl; return 1; } const char* ip = argv[1]; int port = atoi(argv[2]); TCPClientSocket client(ip, port); client.connectToServer(); const char* oob_data = "abc"; const char* normal_data = "123"; int sockfd = client.getSocketFd(); send(sockfd, normal_data, strlen(normal_data), 0); send(sockfd, oob_data, strlen(oob_data), MSG_OOB); send(sockfd, normal_data, strlen(normal_data), 0); return 0; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| #include "TCPServerSocket.h" #include <iostream> #include <stdlib.h> #include <errno.h> #define BUF_SIZE 1024
int main(int argc, char *argv[]){ if(argc <= 2){ std::cout << "Usage: " << basename(argv[0]) << " ip_address port_number" << std::endl; return 1; } const char* ip = argv[1]; int port = atoi(argv[2]); TCPServerSocket server(ip, port); if (server.bindSocket() < 0) { throw std::runtime_error("Failed to bind socket"); } if (server.listenSocket(5) < 0) { throw std::runtime_error("Failed to listen on socket"); } struct sockaddr_in client_addr; socklen_t client_addrlen = sizeof(client_addr); int client_sockfd = server.acceptConnection(&client_addr, &client_addrlen); if (client_sockfd < 0) { throw std::runtime_error("Failed to accept connection"); } char buffer[BUF_SIZE]; memset(buffer, '\0', BUF_SIZE); int ret = recv(client_sockfd, buffer, BUF_SIZE - 1, 0); std::cout << "got " << ret << " bytes of normal data '" << buffer << "'" << std::endl; memset(buffer, '\0', BUF_SIZE); ret = recv(client_sockfd, buffer, BUF_SIZE - 1, 0); std::cout << "got " << ret << " bytes of normal data '" << buffer << "'" << std::endl; close(client_sockfd); return 0; }
|
编译:
1 2
| g++ -o server test_oob_recv.cpp g++ -o client test_oob_send.cpp
|
运行:
打开两个终端,然后先打开服务器端:
1
| ./server 127.0.0.1 12345
|
检查端口使用情况:
1
| netstat -tulnp | grep 12345
|

最后打开客户端:
1
| ./client 127.0.0.1 12345
|

在代码中,客户端先发送给服务器 3 字节的普通数据 123
,然后发送 3 字节带外数据 abc
,最后又发送了 3 字节的普通数据 123
。
然而,仅有最后一个字符“c”被服务器当成真正的带外数据接收。并且,正常数据的接收也被带外数据截断。即前一部分正常数据 123ab
和后续正常数据 123
不能被同一个 recv
调用全部读出。
17.实战3 TCP通信
功能:客户输入小写字符串,服务器返回大写字符串
TCP实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| #include <stdio.h> #include <ctype.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <pthread.h>
#define SERV_PORT 9527
int main(){ char buf[BUFSIZ]; int ret,i;
int lfd = 0, cfd = 0;
struct sockaddr_in serv_addr, clit_addr; socklen_t clit_addr_len;
serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERV_PORT); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
lfd = socket(AF_INET,SOCK_STREAM,0); if(lfd == -1) { perror("socket error"); exit(1); } bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)); listen(lfd,128); clit_addr_len = sizeof(clit_addr); cfd = accept(lfd,(struct sockaddr *)&clit_addr,&clit_addr_len); if(cfd==-1) { perror("accept error"); exit(1); } while(1){ ret = read(cfd,buf,sizeof(buf)); write(STDOUT_FILENO,buf,ret);
for(i=0;i<ret;i++) { buf[i] = toupper(buf[i]); }
write(cfd,buf,ret); }
close(lfd); close(cfd);
return 0; }
|
通过telnet连接

通过客户端连接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| #include <stdio.h> #include <ctype.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <pthread.h>
#define SERV_PORT 9527
int main() { int cfd = 0; char buf[BUFSIZ]; int conter = 11;
struct sockaddr_in serv_addr; serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERV_PORT); inet_pton(AF_INET,"127.0.0.1",&serv_addr.sin_addr.s_addr);
cfd = socket(AF_INET,SOCK_STREAM,0); if(cfd == -1) { perror("socket error"); exit(1); }
int ret = connect(cfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)); if(ret == -1) { perror("connect error"); exit(1); }
while(--conter){ write(cfd,"hello\n",6); ret = read(cfd,buf,sizeof(buf)); write(STDOUT_FILENO,buf,ret); sleep(1); }
close(cfd);
return 0; }
|

18.UDP通信
UDP通信-CSDN博客