
一、网络基础知识
1、Socket 概念
- 1982 - Berkeley Software Distributions 操作系统引入了socket作为本地进程之间通信的接口
- 1986 - Berkeley 扩展了socket 接口,使之支持UNIX 下的TCP/IP 通信
- 现在很多应用 (FTP, Telnet, etc) 都依赖这一接口
- Socket是一个编程接口
- 是一种特殊的文件描述符 (everything in Unix is a file) , bcd-lsp中的s
- 并不仅限于TCP/IP协议
- 面向连接 (Transmission Control Protocol - TCP/IP)
- 无连接 (User Datagram Protocol -UDP 和 Inter-network Packet Exchange - IPX)
【为什么需要Socket?】
- 普通的I/O操作过程 , 打开文件->读/写操作->关闭文件
- TCP/IP协议被集成到操作系统的内核中,引入了新型的"I/O"操作
- 进行网络通信的两个进程在不同的机器上,如何连接?
- 网络协议具有多样性,如何进行统一的操作
- 就需要一种通用的网络编程接口:Socket
2、什么是Socket
独立于具体协议的网络编程接口。在OSI模型中,主要位于会话层和传输层之间BSD Socket(伯克利套接字)是通过标准的UNIX文件描述符和其它程序通讯的一个方法,目前已经被广泛移植到各个平台。
【Socket类型】
- 流式套接字(SOCK_STREAM),提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。
- 数据报套接字(SOCK_DGRAM),提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。
- 原始套接字(SOCK_RAW),可以对较低层次协议如IP、ICMP直接访问。
3、Socket的位置

4、IP地址
IP地址是Internet中主机的标识,Internet中的主机要与别的机器通信必须具有一个IP地址,IP地址为32位(IPv4)或者128位(IPv6),每个数据包都必须携带目的IP地址和源IP地址,路由器依靠此信息为数据包选择路由,表示形式:常用点分形式,如202.38.64.10,最后都会转换为一个32位的无符号整数。IP地址根据网络ID的不同分为5种类型,A类地址、B类地址、C类地址、D类地址和E类地址。
【A类IP地址】一个A类IP地址由1字节的网络地址和3字节主机地址组成,网络地址的最高位必须是"0", 地址范围从1.0.0.0 到126.0.0.0。可用的A类网络有126个,每个网络能容纳1亿多个主机。子网掩码: 255.0.0.0
【B类IP地址】一个B类IP地址由2个字节的网络地址和2个字节的主机地址组成,网络地址的最高位必须是"10",地址范围从128.0.0.0到191.255.255.255。可用的B类网络有16382个,每个网络能容纳6万多个主机 。子网掩码: 255.255.0.0
【C类IP地址】一个C类IP地址由3字节的网络地址和1字节的主机地址组成,网络地址的最高位必须是"110"。范围从192.0.0.0到223.255.255.255。C类网络可达209万余个,每个网络能容纳254个主机。子网掩码: 255.255.255.0
【D类IP地址】用于多播(Multicast)。范围从224.0.0.0到239.255.255.255,D类IP地址第一个字节以"lll0"开始,它是一个专门保留的地址。它并不指向特定的网络,目前这一类地址被用在多点广播(Multicast)中。多点广播地址用来一次寻址一组计算机,它标识共享同一协议的一组计算机。
【E类IP地址】以"llll0"开始,为将来使用保留。 全零("0.0.0.0")地址对应于当前主机。
5、端口号
一个端口号对应一个socket,端口号就是socket的id号。为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理,使用端口号来区别,给socket 一个唯一id号 , 这个类似于进程的pid , 线程tid , 文件的inode号 。TCP端口号与UDP端口号独立,端口号一般由IANA (Internet Assigned Numbers Authority) 管理
- 众所周知端口:1 - 1023(1 - 255之间为众所周知端口,256 - 1023端口通常由UNIX系统占用)
- 已登记端口:1024~49151
- 动态或私有端口:49152~65535
- 无符号短整型的整数(unsigned short ),2个字节的大小。

解释: 数据通过网线到达操作系统的内核, 再经过数据链路与物理层 -> 网络层 -> 传输层 -> 进程1、进程2、进程3 。因为socket 必须要有一个端口号, 就是id号 , 系统根据端口号把数据交给指定的socket即可完成数据通信。进程创建socket时, 会有一个端口号,系统会把和本进程一致端口的数据转发给这个socket , 之后进程可以使用read读这个socket就可以得到接收的数据。
6、字节序
不同类型CPU的主机中,内存存储多字节整数序列有两种方法,称为主机字节序(HBO):
- 小端序(little-endian) - 低序字节存储在低地址,将低字节存储在起始地址,称为"Little-Endian"字节序,Intel、AMD等采用的是这种方式;ARM处理器使用大端和小端都支持的方式 , 默认小端
- 大端序(big-endian)- 高序字节存储在低地址,将高字节存储在起始地址,称为"Big-Endian"字节序,由PowerPC、Motorola等所采用
网络字节序(NBO - Network Byte Order),网络中传输的数据必须按网络字节序,即大端字节序,在大部分PC机上,当应用进程将整数送入socket前,需要转化成网络字节序;总结 : 小端更加符合人的逻辑思维

7、字节序转换函数
把给定系统所采用的字节序称为主机字节序。为了避免不同类别主机之间在数据交换时由于对于字节序的不同而导致的差错,引入了网络字节序。主机字节序到网络字节序
- u_long htonl (u_long hostlong);
- u_short htons (u_short short);
网络字节序到主机字节序
- u_long ntohl (u_long hostlong);
- u_short ntohs (u_short short);
二、网络编程的系统调用(API)
1、专有名词解释
- **POSIX(可移植操作系统接口)**是一个公认的工业标准。规范中的操作系统是POSIX兼容的。
- API : 应用程序接口(Application Programming Interface)
2、socket 创建套接字
cpp
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
函数功能:创建一个socket 进行通信,创建一个文件,用于网络通信 , 等同于open , 这个文件存在内存中
函数参数:【domain】,要做什么类型的通信,AF_UNIX, AF_LOCAL : Local communication , 做本地间进程通信;AF_INET : IPv4 Internet protocols , IPV4 的通信;AF_INET6 :IPv6 Internet protocols , IPV6的通信。【type】,创建socket 的类型,SOCK_STREAM : 流式套接字 , 如果网络通信使用tcp的协议;SOCK_DGRAM : 用户数据报套接字 ,如果网络通信使用UDP的协议;SOCK_RAW : 原始套接字 , 使用 IPX协议。【protocol】 :通信的协议,通常设置为 0
函数返回值:成功返回一个文件描述符,失败返回-1 并设置errno,On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set appropriately.
3、bind 绑定本机地址和端口
cpp
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
函数功能:给创建的socket 一个固定的端口, IP就是本机的IP地址 , 如果不给固定的端口, 这个端口是随机的。如果这个socket 作为服务器, 必须有一个固定不变的端口, 让客户端可以找到服务器。
函数参数:【sockfd】,要操作的socket 的文件;【addr】,创建socket 的类型;【addrlen】:结构的大小 , sizeof(struct sockaddr)
函数返回值:成功返回0,失败返回-1 并设置errno,On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
cpp
struct sockaddr {
sa_family_t sa_family; // 2字节
char sa_data[14]; // 14字节
} // 这个结构体一共占用16字节
#include <arpa/inet.h>
struct sockaddr_in // _in : internet
{
__SOCKADDR_COMMON (sin_); // 协议簇 , 占用2字节
in_port_t sin_port; /* Port number. */ // 16位的端口号 2字节
struct in_addr sin_addr; /* Internet address. */ // IP地址 4字节
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)]; // 保留8字节
}; // 16字节大小
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr; // 无符号32位的ip地址 , 占用4字节
};
【addrlen】:结构的大小 , sizeof(struct sockaddr)
函数返回值:成功返回0,失败返回-1 并设置errno,On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
4、listen 设置监听套接字
cpp
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
函数功能:设置这个socket 为监听socket , 让这个socket变成服务器的socket,可以接收客户端发送的请求
函数参数:【sockfd】,要操作的socket 的文件。【backlog】,等待队列的长度 , 客户端请求队列的长度
函数返回值:成功返回0,失败返回-1 并设置errno,On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
5、inet_addr 字符串转网络二进制
cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
函数功能:把点分十进制的字符串转换成网络二进制ip
函数参数:【cp】, 要转换的ip字符串
函数返回值:成功返回网络字节序二进制ip,失败返回-1
6、inet_ntoa 网络二进制转字符串
cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
函数功能:把网络二进制的ip地址转成点分十进制的字符串
函数参数:【in】, ip地址的结构体
函数返回值:成功返回,IPV4的字符串
7、实例socket创建
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char const *argv[])
{
// ./app ip port
if(argc != 3)
{
printf("Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("socket");
exit(1);
}
printf("sockfd=%d\n", sockfd);
// bind 固定的ip和端口
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET; // 协议族
servaddr.sin_addr.s_addr = inet_addr(argv[1]); // ip地址
servaddr.sin_port = htons(atoi(argv[2])); // 端口号 主机字节序转换网络字节序
int ret = bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (ret == -1)
{
perror("bind");
exit(1);
}
// listen
ret = listen(sockfd, 5);
if (ret == -1)
{
perror("listen");
exit(1);
}
system("netstat -ant"); // 查看监听端口
close(sockfd); // 关闭监听套接字
return 0;
}

8、accept 接收tcp的连接请求
cpp
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
函数功能:接收客户端的一个连接请求,返回一个已连接的socket和客户端进行一对一的通信,服务器的socket 是公司的前台,客户端就是访客,客户端先发送请求给服务器的socket(这个socket有固定的端口和ip) , 前台(服务器socket)会给你找到一个人和客户端一对一服务。这个过程中一共用到3个socket,客户端的socket,给客户端一对一服务的socket , accept返回的socket, 服务器的socket
函数参数:【sockfd】, 要操作的socket 的文件;【addr】,客户端的地址信息;【addrlen】,客户端地址的大小
函数返回值:成功返回返回一个和客户端建立连接的socket , 一般newsockfd,失败返回-1并设置errno,On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
9、connect 发起TCP的连接
cpp
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数功能:connect发起一个TCP的连接请求 , 是TCP建立连接的第一次握手,服务器accept 收到这个连接请求后 , 再给connect 发送一个回复, TCP建立连接的第二次握手,再次发送一个回复建立连接的请求 , TCP建立连接的第三次握手
函数参数:【sockfd】,要操作的socket 的文件;【addr】,服务端的地址信息;【addrlen】,服务端地址的大小
函数返回值:成功返回0,失败返回-1 并设置errno,If the connection or binding succeeds, zero is returned. On error, -1 is returned, and errno is set appropriately.
10、recv 接收数据
cpp
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
函数功能:从socket 中读数据,这个函数和read 函数实现的功能一致 , 大部分时候可以等价替换
函数参数:【sockfd】,要操作的socket 的文件;【buf】,读回来数据的存放地址;【len】,读多少字节;flags,一般为 0
函数返回值:成功返回实际读回来的字节数,失败返回-1 并设置errno,These calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate the error.
11、send 发送数据
cpp
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, void *buf, size_t len, int flags);
函数功能:写数据到socket的发送区,这个函数和write 函数实现的功能一致 , 大部分时候可以等价替换
函数参数:【sockfd】, 要操作的socket 的文件;【buf】,要写数据内存地址;【len】,写多少字节;flags, 一般为 0
函数返回值:成功返回实际发送的字节数,失败返回-1 并设置errno,On success, these calls return the number of bytes sent. On error, -1 is returned, and errno is set appropriately.
- read()和write()经常会代替recv()和send(),通常情况下,看程序员的偏好
- 使用read()/write()和recv()/send()时最好统一
12、close 套接字的关闭
cpp
#include <unistd.h>
int close(int fd);
函数功能:关闭双向通讯
函数参数:【fd】,要关闭的socket文件
函数返回值:成功返回0,失败返回-1 并设置errno
13、shutdown 套接字的关闭
cpp
#include <sys/socket.h>
int shutdown(int sockfd, int how);
函数功能:TCP连接是双向的(是可读写的),当我们使用close时,会把读写通道都关闭,有时侯我们希望只关闭一个方向,这个时候我们可以使用shutdown。针对不同的howto,系统回采取不同的关闭方式。
函数参数:【sockfd】,要关闭的socket文件。【how】,如何关闭,howto = 0 : 关闭读通道,但是可以继续往套接字写数据。howto = 1 : 和上面相反,关闭写通道。只能从套接字读取数据。howto = 2 : 关闭读写通道,和close()一样
函数返回值:成功返回0,失败返回-1 并设置errno,On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
14、sendto 发送数据
cpp
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
函数功能:发送数据到指定的地址上 , 这个函数一般在使用UDP协议时使用。因为udp 不需要三次握手建立连接,可以直接发送数据
函数参数:【sockfd】,要发送数据的socket文件。【buf】,要发送数据的内存地址。【len】,要发送数据的字节数。【flags】, 发送方式一般为0。【dest_addr】,要发送目的地址, 一般是服务器地址。【addrlen】, 地址结构体的大小
函数返回值:成功返回发送的字节数,失败返回-1 并设置errno,On success, these calls return the number of bytes sent. On error, -1 is returned, and errno is set appropriately.
15、recvfrom 接收数据
cpp
#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);
函数功能:从指定的socket文件中读数据 , 这个函数一般在使用UDP协议时使用。因为udp 不需要三次握手建立连接,可以直接接收数据
函数参数:【sockfd】,要接收数据的socket文件。【buf】,接收数据要保存的内存地址。len, 接收数据的字节数。【flags】,接收方式一般为0。【src_addr】,发送数据的源地址, 一般是客户端的地址。【addrlen】,地址结构体的大小
函数返回值:成功返回发接收的字节数,失败返回-1 并设置errno,These calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate the error.
三、TCP服务器与客户端
1、TCP服务端流程


2、实例TCP服务器
- tcp_server
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define NSize 128
int main(int argc, char const *argv[])
{
char buf[NSize] = {0};
int ret, connfd;
// ./app ip port
if(argc != 3)
{
printf("Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("socket");
exit(1);
}
printf("sockfd=%d\n", sockfd);
// bind 固定的ip和端口
struct sockaddr_in servaddr, cliaddr;
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
servaddr.sin_family = AF_INET; // 协议族
servaddr.sin_addr.s_addr = inet_addr(argv[1]); // ip地址
servaddr.sin_port = htons(atoi(argv[2])); // 端口号 主机字节序转换网络字节序
ret = bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (ret == -1)
{
perror("bind");
exit(1);
}
// listen
ret = listen(sockfd, 5);
if (ret == -1)
{
perror("listen");
exit(1);
}
system("netstat -ant"); // 查看监听端口
socklen_t cliaddr_len = sizeof(cliaddr);
while (1)
{
connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
if (connfd == -1)
{
perror("accept");
exit(1);
}
printf("connfd=%d\n", connfd);
printf("cliaddr=%s, port=%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
ret = recv(connfd, buf, NSize, 0);
if (ret == -1)
{
perror("recv");
exit(1);
}
printf("recv %d bytes: %s\n", ret, buf);
ret = send(connfd, buf, ret, 0);
if (ret == -1)
{
perror("send");
exit(1);
}
printf("send %d bytes: %s\n", ret, buf);
close(connfd);
}
close(sockfd); // 关闭监听套接字
return 0;
}

3、TCP客户端流程

4、实例TCP客户端
- tcp_client
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define NSize 128
int main(int argc, char const *argv[])
{
char buf[NSize] = {0};
int ret, connfd;
// ./app ip port
if (argc != 3)
{
printf("Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("socket");
exit(1);
}
printf("sockfd=%d\n", sockfd);
// bind 固定的ip和端口
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET; // 协议族
servaddr.sin_addr.s_addr = inet_addr(argv[1]); // ip地址
servaddr.sin_port = htons(atoi(argv[2])); // 端口号 主机字节序转换网络字节序
// connect
ret = connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (ret == -1)
{
perror("connect");
exit(1);
}
printf(">:");
fgets(buf, NSize, stdin);
buf[strlen(buf) - 1] = '\0'; // 去掉换行符
ret = send(sockfd, buf, strlen(buf), 0);
if (ret == -1)
{
perror("send");
exit(1);
}
ret = recv(sockfd, buf, NSize, 0);
if (ret == -1)
{
perror("recv");
exit(1);
}
printf("recv %d bytes: %s\n", ret, buf);
close(sockfd); // 关闭监听套接字
return 0;
}


5、TCP服务器和客户端传输数据
- tcp_server_talk
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define NSize 128
int main(int argc, char const *argv[])
{
char buf[NSize] = {0};
int ret, connfd;
// ./app ip port
if (argc != 3)
{
printf("Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("socket");
exit(1);
}
printf("sockfd=%d\n", sockfd);
// bind 固定的ip和端口
struct sockaddr_in servaddr, cliaddr;
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
servaddr.sin_family = AF_INET; // 协议族
servaddr.sin_addr.s_addr = inet_addr(argv[1]); // ip地址
servaddr.sin_port = htons(atoi(argv[2])); // 端口号 主机字节序转换网络字节序
ret = bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (ret == -1)
{
perror("bind");
exit(1);
}
// listen
ret = listen(sockfd, 5);
if (ret == -1)
{
perror("listen");
exit(1);
}
socklen_t cliaddr_len = sizeof(cliaddr);
while (1)
{
connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
if (connfd == -1)
{
perror("accept");
exit(1);
}
printf("connfd=%d\n", connfd);
while (1)
{
ret = recv(connfd, buf, NSize, 0);
if (ret == -1)
{
perror("recv");
exit(1);
}
// quit
if (strncmp(buf, "quit", 4) == 0)
{
printf("%s %d quit\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
break;
}
printf("%s %d said: %s\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), buf);
printf("server>:");
fgets(buf, NSize, stdin);
buf[strcspn(buf, "\n")] = '\0';// 去掉换行符
ret = send(connfd, buf, strlen(buf), 0);
if (ret == -1)
{
perror("send");
exit(1);
}
memset(buf, 0, NSize);
}
close(connfd);
}
close(sockfd); // 关闭监听套接字
return 0;
}
- tcp_client_talk
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define NSize 128
int main(int argc, char const *argv[])
{
char buf[NSize] = {0};
int ret, connfd;
// ./app ip port
if (argc != 3)
{
printf("Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("socket");
exit(1);
}
printf("sockfd=%d\n", sockfd);
// bind 固定的ip和端口
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET; // 协议族
servaddr.sin_addr.s_addr = inet_addr(argv[1]); // ip地址
servaddr.sin_port = htons(atoi(argv[2])); // 端口号 主机字节序转换网络字节序
// connect
ret = connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (ret == -1)
{
perror("connect");
exit(1);
}
while (1)
{
printf(">:");
fgets(buf, NSize, stdin);
buf[strlen(buf) - 1] = '\0'; // 去掉换行符
ret = send(sockfd, buf, strlen(buf), 0);
if (ret == -1)
{
perror("send");
exit(1);
}
// quit
if (strncmp(buf, "quit", 4) == 0)
{
printf("quit\n");
break;
}
ret = recv(sockfd, buf, NSize, 0);
if (ret == -1)
{
perror("recv");
exit(1);
}
printf("recv %d bytes: %s\n", ret, buf);
}
close(sockfd); // 关闭监听套接字
return 0;
}

四、TCP握手/挥手的过程分析
1、TCP连接建立
在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。服务器必须准备好接受外来的连接。这通过调用socket、 bind和listen函数来完成。客户端的初始序列号为 J,而服务器的初始序列号为K
- 第一次握手:客户通过调用connect进行主动连接。这引起客户TCP发送一个SYN(表示同步)SYN = 1 和一个J客户端初始序列号。并进入SYN_SEND状态,等待服务器的确认。
- 第二次握手:服务器必须确认客户的SYN ,服务器以单个字节向客户发送SYN和对客户SYN的ACK(表示确认),此时服务器进入SYN_RECV状态 。
- 第三次握手:客户收到服务器的SYN+ACK。向服务器发送确认分节,客户端和服务器端都进入ESTABLISHED(成功连接)状态,完成三次握手。

2、TCP连接终止
TCP的4次挥手,TCP连接终止需四个分节。
- 第一次握手:某个应用进程首先调用close,我们称这一端执行主动关闭。这一端的TCP于是发送一个FIN分节,表示数据发送完毕。
- 第二次握手:接收到FIN的另一端执行被动关闭(passive close)。这个FIN由TCP确认。
- 第三次握手:一段时间后,接收到文件结束符的应用进程将调用close关闭它的套接口。这导致它的TCP也发送一个FIN。
- 第四次握手:接收到这个FIN的原发送端TCP对它进行确认。
4次挥手中的最后2次可以没有
五、UDP 服务器和客户端
1、UDP服务器端流程

2、UDP客户端流程

cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define NSize 128
int main(int argc, char const *argv[])
{
char buf[NSize] = {0};
int ret, connfd;
// ./app ip port
if (argc != 3)
{
printf("Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
// udp 套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1)
{
perror("socket");
exit(1);
}
printf("sockfd=%d\n", sockfd);
// bind 固定的ip和端口
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET; // 协议族
servaddr.sin_addr.s_addr = inet_addr(argv[1]); // ip地址
servaddr.sin_port = htons(atoi(argv[2])); // 端口号 主机字节序转换网络字节序
while (1)
{
printf(">:");
fgets(buf, NSize, stdin);
buf[strlen(buf) - 1] = '\0'; // 去掉换行符
ret = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (ret == -1)
{
perror("send");
exit(1);
}
// quit
if (strncmp(buf, "quit", 4) == 0)
{
printf("quit\n");
break;
}
ret = recvfrom(sockfd, buf, NSize, 0, NULL, NULL); // 不保存服务器信息, 因为已经知道服务器地址
if (ret == -1)
{
perror("recv");
exit(1);
}
printf("%s&%d:%s\n", inet_ntoa(servaddr.sin_addr), ntohs(servaddr.sin_port), buf);
memset(buf, 0, sizeof(buf));
}
close(sockfd); // 关闭监听套接字
return 0;
}
3、实例udp 服务器
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define NSize 128
int main(int argc, char const *argv[])
{
char buf[NSize] = {0};
int ret, connfd;
// ./app ip port
if(argc != 3)
{
printf("Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
// udp 套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1)
{
perror("socket");
exit(1);
}
printf("sockfd=%d\n", sockfd);
// bind 固定的ip和端口
struct sockaddr_in servaddr, cliaddr;
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
servaddr.sin_family = AF_INET; // 协议族
servaddr.sin_addr.s_addr = inet_addr(argv[1]); // ip地址
servaddr.sin_port = htons(atoi(argv[2])); // 端口号 主机字节序转换网络字节序
ret = bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (ret == -1)
{
perror("bind");
exit(1);
}
socklen_t cliaddr_len = sizeof(cliaddr);
while (1)
{
ret = recvfrom(sockfd, buf, NSize, 0, (struct sockaddr *)&cliaddr, &cliaddr_len);
if (ret == -1)
{
perror("recv");
exit(1);
}
// 打印客户端地址和端口
printf("%s&%d:%s\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), buf);
ret = sendto(sockfd, buf, ret, 0, (struct sockaddr *)&cliaddr, cliaddr_len);
if (ret == -1)
{
perror("send");
exit(1);
}
memset(buf, 0, sizeof(buf));
}
close(sockfd); // 关闭监听套接字
return 0;
}

六、wireshark 抓包
1、Wireshark介绍

Wireshark是一款开源的网络协议分析工具,它可以用于捕获和分析网络数据包。它能够帮助网络管理员和安全专家监测和解决网络问题,同时也可以用于网络故障排除、网络性能优化、网络安全分析等方面。Wireshark支持多种操作系统,包括Windows、MacOS和Linux。它能够捕获网络流量,并将其以可读的形式显示出来,以便用户分析和理解。它还提供了强大的过滤和搜索功能,使用户可以根据特定的协议、源地址、目标地址等条件来筛选和查找数据包。除了捕获和分析网络数据包外,Wireshark还支持解析和显示各种网络协议,包括以太网、TCP/IP、HTTP、DNS、SSL、SSH等。它能够展示每个数据包的详细信息,包括源和目标IP地址、端口号、协议类型、数据内容等。总之,Wireshark是一个强大而灵活的网络分析工具,适用于网络管理、网络安全、网络开发等领域。无论是初学者还是专业人士,都可以通过使用Wireshark来深入了解和分析网络通信。
2、Wireshark安装
按照系统版本选择下载,下载完成后,按照软件提示一路Next安装。

3、Wireshark抓取TCP握手与挥手
关闭Ubuntu系统的软件, VSCode需要关闭,避免多余的数据包。打开wireshark,主界面如下:

点击捕获选项

设置过滤选项

关闭VSCode,因为VSCode通过ssh链接的Ubuntu,有多余的数据。我的联网使用的以太网,如果你的是通过WIFI请选择WLAN接口连接。点击开始, 开始捕捉数据包。

运行tcp服务器代码

在window端, 打开网络调试助手, 填写上信息后点击链接。

分析并抓取TCP的三次握手。

4、Wireshark抓取UDP数据
Ubuntu中运行UDP服务器代码

UDP发送数据包

UDP接收数据包

七、I/O模型
1、IO模型
在UNIX/Linux下主要有4种I/O 模型:
- 阻塞I/O:最常用、最简单、效率最低
- 非阻塞I/O:可防止进程阻塞在I/O操作上,需要轮询
- I/O 多路复用:允许同时对多个I/O进行控制
- 信号驱动I/O:一种异步通信模型
2、阻塞I/O 模式
阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。缺省情况下,套接字建立后所处于的模式就是阻塞I/O 模式。前面学习的很多读写函数在调用过程中会发生阻塞。读操作中的read、recv、recvfrom,写操作中的write、send,其他操作:accept、connect。以read函数为例:
- 进程调用read函数从套接字上读取数据,当套接字的接收缓冲区中还没有数据可读,函数read将发生阻塞。
- 它会一直阻塞下去,等待套接字的接收缓冲区中有数据可读。
- 经过一段时间后,缓冲区内接收到数据,于是内核便去唤醒该进程,通过read访问这些数据。
- 如果在进程阻塞过程中,对方发生故障,那这个进程将永远阻塞下去。
写阻塞,在写操作时发生阻塞的情况要比读操作少。主要发生在要写入的缓冲区的大小,小于要写入的数据量的情况下。这时,写操作不进行任何拷贝工作,将发生阻塞。一旦发送缓冲区内有足够的空间,内核将唤醒进程,将数据从用户缓冲区中拷贝到相应的发送数据缓冲区。UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在发送缓冲区满的情况,在UDP套接字上执行的写操作永远都不会阻塞。
3、非阻塞模式I/O
当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:"当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。"当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。这种模式使用中不普遍。

非阻塞模式的实现,使用fcntl()函数来实现非阻塞模型,当你一开始建立一个套接字描述符的时候,系统内核将其设置为阻塞IO模式。可以使用函数fcntl()设置一个套接字的标志为O_NONBLOCK 来实现非阻塞。
cpp
int flag ;
flag = fcntl(sockfd, F_GETFL, 0); // 获取sockfd 的属性
flag |= O_NONBLOCK; // 属性中添加 O_NONBLOCK属性(非阻塞)
fcntl(sockfd, F_SETFL, flag); //设置sockfd 的属性
4、多路复用I/O
应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的;若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;若设置多个进程,分别处理一条数据通路,将新产生进程间的同步与通信问题,使程序变得更加复杂;比较好的方法是使用I/O多路复用。其基本思想是:把阻塞和非阻塞合起来实现的一种模型 , 先构造一张有关文件描述符的表,然后调用一个函数。当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。
5、异步IO模型
就是进程间通信机制之信号signal 的模型就是异步IO
6、select 实现多路复用
cpp
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
函数功能:实现多路复用的机制,同poll 函数一致 , 实现多路复用。在我们调用select时进程会一直阻塞直到以下的一种情况发生,有文件可以读,有文件可以写,超时所设置的时间到。
函数参数:【nfds】,所有监控的文件描述符中最大的那一个加1;【readfds】,监控要读的文件表(数组);【writefds】,监控要写的文件表(数组);【exceptfds】,监控其他的文件表(数组);【timeout】,超时时间,NULL不设置超时时间 , 阻塞模式,非NULL设置超时时间。
cpp
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
}
函数返回值:成功返回可操作文件的个数大于0,等于0超时时间到,失败返回 : -1 并设置errno。On error, -1 is returned, and errno is set to indicate the error; the file descriptor sets are unmodified, and timeout becomes undefined.
7、设置文件描述符的宏函数
cpp
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
函数功能:FD_CLR,将fd从fdset里面清除;FD_SET,将fd加入到fdset;FD_ZERO,从fdset中清除所有的文件描述符;FD_ISSET判断fd是否在fdset集合中是准备就绪(是否是可操作)
函数参数:【fd】,要操作的文件;【set】,要监控的表(一个数组)
返回值:FD_ISSET,文件可操作时,返回为真;文件不可操作时, 返回假
8、实例监控多个文件的读
监控标准输入(0 和 监听socket )
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#define NSize 128
int main(int argc, char const *argv[])
{
char buf[NSize] = {0};
int ret, connfd;
// ./app ip port
if (argc != 3)
{
printf("Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("socket");
exit(1);
}
printf("sockfd=%d\n", sockfd);
// bind 固定的ip和端口
struct sockaddr_in servaddr, cliaddr;
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
servaddr.sin_family = AF_INET; // 协议族
servaddr.sin_addr.s_addr = inet_addr(argv[1]); // ip地址
servaddr.sin_port = htons(atoi(argv[2])); // 端口号 主机字节序转换网络字节序
ret = bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (ret == -1)
{
perror("bind");
exit(1);
}
// listen
ret = listen(sockfd, 5);
if (ret == -1)
{
perror("listen");
exit(1);
}
socklen_t cliaddr_len = sizeof(cliaddr);
// 定义可读集合
fd_set readfds;
while (1)
{
FD_ZERO(&readfds); // 清空可读集合
FD_SET(0, &readfds); // 把 标准输入加入到 可读集合中
FD_SET(sockfd, &readfds); // 把 监听套接字加入到 可读集合中
// 执行这个函数后, 会轮询监控表中的文件状态, 如果有可读立即返回可读文件数量
// 如果没有可读文件, 会阻塞等待, 直到有可读文件
ret = select(sockfd + 1, &readfds, NULL, NULL, NULL);
if (ret == -1)
{
perror("select");
exit(1);
}
for (int i = 0; i < sockfd + 1; i++)
{
// 监听套接字可读, 说明有新的连接请求
if (i == sockfd && FD_ISSET(i, &readfds))
{
connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
if (connfd == -1)
{
perror("accept");
exit(1);
}
printf("connfd=%d\n", connfd);
printf("cliaddr=%s, port=%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
close(connfd); // 关闭连接套接字
}
// 标准输入可读, 说明有用户输入
else if (i == 0 && FD_ISSET(i, &readfds))
{
fgets(buf, NSize, stdin);
buf[strlen(buf) - 1] = '\0'; // 去掉字符串末尾的换行符
printf("readfrom stdin:%s\n", buf);
}
}
}
close(sockfd); // 关闭监听套接字
return 0;
}

八、服务器模型
1、服务器模型概念
在网络程序里面,通常都是一个服务器处理多个客户机。为了处理多个客户机的请求,服务器端的程序有不同的处理方式。目前最常用的服务器模型
- 循环服务器 : 循环服务器在同一个时刻只能响应一个客户端的请求
- 并发服务器 : 并发服务器在同一个时刻可以响应多个客户端的请求
2、循环服务器模型
TCP服务器端运行后等待客户端的连接请求。TCP服务器接受一个客户端的连接后开始处理,完成了客户的所有请求后断开连接。TCP循环服务器一次只能处理一个客户端的请求。只有在当前客户的所有请求都完成后,服务器才能处理下一个客户的连接/服务请求。如果某个客户端一直占用服务器资源,那么其它的客户端都不能被处理。TCP服务器一般很少采用循环服务器模型。
cpp
socket(...);
bind(...);
listen(...);
while(1)
{
newsockfd = accept(...);
while(1)
{
recv(...);
process(...);
send(...);
}
close(...);
}
UDP循环服务器,UDP服务器每次从套接字上读取一个客户端的请求,处理后将结果返回给客户机。因为UDP是无连接, 不可靠通信, 可以接收不同客户端的请求。因此这种服务器用的比较多。可以用下面的算法来实现:
cpp
socket(...);
bind(...);
while(1)
{
recvfrom(...);
process(...);
sendto(...);
}
3、并发服务器模型
TCP并发服务器,为了弥补TCP循环服务器的缺陷,人们又设计了并发服务器的模型。并发服务器的设计思想是服务器接受客户端的连接请求后创建子进程来为客户端服务,TCP并发服务器可以避免TCP循环服务器中客户端独占服务器的情况。为了响应客户机的请求,服务器要创建子进程来处理。 如果有多个客户端的话,服务器端需要创建多个子进程。过多的子进程会影响服务器端的运行效率。
cpp
socket(...);
bind(...);
listen(...);
while(1)
{
newsockfd = accept(...);
if (fork() = = 0) // 子进程
{
while(1)
{
recv(...);
process(...);
send(...);
}
close(...); // 断开和客户端的连接
exit(...); // 退出子进程
}
close(...); // 父进程 关闭newsockfd
}
UDP并发服务器,人们把并发的概念用于UDP就得到了UDP并发服务器模型。UDP并发服务器模型和TCP服务器模型一样,创建一个子进程来处理客户端的请求。除非UDP服务器在处理某个客户端的请求时所用的时间比较长,人们实际上较少用这种模型。
4、实例TCP fork并发服务器
cpp
socket(...);
bind(...);
listen(...);
while(1)
{
newsockfd = accept(...);
if (fork() = = 0) // 子进程
{
while(1)
{
recv(...);
process(...);
send(...);
}
close(...); // 断开和客户端的连接
exit(...); // 退出子进程
}
close(...); // 父进程 关闭newsockfd
}
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#define NSize 128
int main(int argc, char const *argv[])
{
char buf[NSize] = {0};
int ret, connfd;
// ./app ip port
if (argc != 3)
{
printf("Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
signal(SIGCHLD, SIG_IGN); // 忽略子进程退出信号
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("socket");
exit(1);
}
printf("sockfd=%d\n", sockfd);
// bind 固定的ip和端口
struct sockaddr_in servaddr, cliaddr;
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
servaddr.sin_family = AF_INET; // 协议族
servaddr.sin_addr.s_addr = inet_addr(argv[1]); // ip地址
servaddr.sin_port = htons(atoi(argv[2])); // 端口号 主机字节序转换网络字节序
ret = bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (ret == -1)
{
perror("bind");
exit(1);
}
// listen
ret = listen(sockfd, 5);
if (ret == -1)
{
perror("listen");
exit(1);
}
socklen_t cliaddr_len = sizeof(cliaddr);
while (1)
{
connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
if (connfd == -1)
{
perror("accept");
exit(1);
}
printf("connfd=%d\n", connfd);
// 创建子进程 , 子进程相应客户端的请求
if (fork() == 0)
{
while (1)
{
ret = recv(connfd, buf, NSize, 0);
if (ret == -1)
{
perror("recv");
exit(1);
}
// quit
if (strncmp(buf, "quit", 4) == 0)
{
printf("%s %d quit\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
break;
}
printf("%s %d said: %s\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), buf);
printf("server>:");
fgets(buf, NSize, stdin);
buf[strcspn(buf, "\n")] = '\0'; // 去掉换行符
ret = send(connfd, buf, strlen(buf), 0);
if (ret == -1)
{
perror("send");
exit(1);
}
memset(buf, 0, NSize);
}
close(connfd); // 关闭连接套接字
exit(0); // 子进程退出
}
close(connfd); // 关闭连接套接字 , fork 后有2个, 要关闭1个
}
close(sockfd); // 关闭监听套接字
return 0;
}

5、实例IO多路复用TCP并发服务器
cpp
#define N 128
int listenfd, connectfd, maxfd, i;
struct sockaddr_in servaddr, client_addr;
socklen_t len;
char buf[N];
fd_set rdfs, tempfs;
listenfd = socket()
bind()
listen(listenfd, 5);
FD_ZERO(&rdfs);
FD_SET(listenfd, &rdfs);
maxfd = listenfd;
while (1)
{
tempfs = rdfs; // 这里很重要,为什么?
select(tempfs)
for (i = 0; i <= maxfd; i++)
{
if (FD_ISSET(i, &tempfs))
{
if (i == listenfd) // 有新客户端建立连接
{
connectfd = accept()
printf();
FD_SET(connectfd); // 更新文件描述符,更新给谁?
maxfd = maxfd > connectfd ? maxfd : connectfd;
}
else // 如果不是客户端建立的连接, 那就是有客户端发送来数据, 此时要接收数据
{
if( recv(i, buf, N, 0) == 0) // 对方关闭
{
close(); //
FD_CLR(); // 清除
}
else
{
send(); // 发送数据
}
}
}
}
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#define NSize 128
int main(int argc, char const *argv[])
{
char buf[NSize] = {0};
int ret, connfd;
// ./app ip port
if (argc != 3)
{
printf("Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("socket");
exit(1);
}
printf("sockfd=%d\n", sockfd);
// bind 固定的ip和端口
struct sockaddr_in servaddr, cliaddr;
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
servaddr.sin_family = AF_INET; // 协议族
servaddr.sin_addr.s_addr = inet_addr(argv[1]); // ip地址
servaddr.sin_port = htons(atoi(argv[2])); // 端口号 主机字节序转换网络字节序
ret = bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (ret == -1)
{
perror("bind");
exit(1);
}
// listen
ret = listen(sockfd, 5);
if (ret == -1)
{
perror("listen");
exit(1);
}
socklen_t cliaddr_len = sizeof(cliaddr);
// 定义可读集合
fd_set readfds, tempfs;
FD_ZERO(&readfds); // 清空可读集合
FD_SET(sockfd, &readfds); // 把 监听套接字加入到 可读集合中
// 定义1个最大文件描述符
int maxfd = sockfd;
while (1)
{
// 因为使用 select 后, 会修改可读集合, 所以需要备份一下
tempfs = readfds;
// 执行这个函数后, 会轮询监控表中的文件状态, 如果有可读立即返回可读文件数量
// 如果没有可读文件, 会阻塞等待, 直到有可读文件
ret = select(maxfd + 1, &tempfs, NULL, NULL, NULL);
if (ret == -1)
{
perror("select");
exit(1);
}
for (int i = 0; i < maxfd + 1; i++)
{
// 监听套接字可读, 说明有新的连接请求
if (i == sockfd && FD_ISSET(i, &tempfs))
{
connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
if (connfd == -1)
{
perror("accept");
exit(1);
}
printf("connfd=%d\n", connfd);
printf("cliaddr=%s, port=%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
// 把connfd 加入到集合内
FD_SET(connfd, &readfds);
// 三目运算符 选择最大的文件描述符
maxfd = connfd > maxfd ? connfd : maxfd;
}
// 其他套接字可读, 说明有数据可读
else if (i != sockfd && FD_ISSET(i, &tempfs))
{
ret = recv(i, buf, NSize, 0);
if (ret == -1)
{
perror("recv");
exit(1);
}
else if (ret == 0)
{
printf("client closed\n");
FD_CLR(i, &readfds); // 从集合中移除
close(i); // 关闭连接套接字
} // ret > 0 有接收的数据
else
{
printf("read %d bytes: %s\n", ret, buf);
}
}
}
}
close(sockfd); // 关闭监听套接字
return 0;
}
