在进行TCP编程之前,我们先来了解一下socket,也就是之前在进程间通信这篇文章中涉及到的套接字( 具体可以点击链接查看复习进程间通信-IPC),以及端口号和字节序。
1.socket:套接字
1.1 socket发展
1、1982 - Berkeley Software Distributions 操作系统引入了socket作为本地进程之间通信的接口
2、1986 - Berkeley 扩展了socket 接口,使之支持UNIX 下的TCP/IP 通信
3、现在很多应用 (FTP, Telnet) 都依赖这一接口
1.2 socket介绍
1、是一个编程接口
2、是一种特殊的文件描述符 (everything in Unix is a file)
3、socket是一种通信机制,并不仅限于TCP/IP协议
4、面向连接 (Transmission Control Protocol - TCP/IP)
5、无连接 (User Datagram Protocol -UDP 和 Inter-network Packet Exchange - IPX)
1.3 为什么需要socket?
1、普通的I/O操作过程 :打开文件->读/写操作->关闭文件
2、TCP/IP协议被集成到操作系统的内核中,引入了新型的“I/O”操作 ----->进行网络通信的两个进程在不同的机器上,如何连接? 网络协议具有多样性,如何进行统一的操作 ?
需要一种通用的网络编程接口:Socket
1.4 socket类型
流式套接字(SOCK_STREAM) ---> TCP
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流。
数据报套接字(SOCK_DGRAM) --> UDP
提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送可能乱序接收。
原始套接字(SOCK_RAW)
可以对较低层次协议如IP、ICMP直接访问,还有一些ping命令
1.5 位置
2. 端口号
- 为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理,使用端口号来区分TCP端口号与UDP端口号独立(UDP port为8888,TCP port也可为8888 )
- 端口号一般由IANA (Internet Assigned Numbers Authority) 管理
- 端口用两个字节来表示
众所周知端口(被占用:1~1023(1~255之间为众所周知端口,256~1023端口通常由UNIX系统占用))
已登记端口:1024~49151(----可用来建立与其它主机的会话----)
动态或私有端口:49152~65535 --固定某些服务使用--
3.字节序
1.字节序: 不同类型的cpu主机,内存存储大于一个字节类型的数据在内存中的存放顺序。
2.浮点类型,字符类型,字符串没有字节序
3.分类:
小端序(little-endian) - 低序字节存储在低地址 (主机字节序)
大端序(big-endian)- 高序字节存储在低地址 (网络字节序)
网络中传输的数据必须使用网络字节序,即大端字节序
终端显示的数据必须是主机字节序,即小端字节序
3.1 端口转换
主机字节序转换为网络字节序 (小端序->大端序)
u_long htonl (u_long hostlong); //host to network long
u_short htons (u_short short); //掌握这个
网络字节序转换为主机字节序(大端序->小端序)
u_long ntohl (u_long hostlong);
u_short ntohs (u_short short);
3.2 IP地址转换
主机字节序转换为网络字节序 (小端序->大端序)
in_addr_t inet_addr(const char *strptr); //该参数是字符串
typedef uint32_t in_addr_t;
struct in_addr {
in_addr_t s_addr;
};
功能: 主机字节序转为网络字节序
参数: const char *strptr: 字符串
返回值: 返回一个无符号长整型数(无符号32位整数用十六进制表示),
否则NULL
网络字节序转换为主机字节序(大端序->小端序)
char *inet_ntoa(stuct in_addr inaddr);
功能: 将网络字节序二进制地址转换成主机字节序。
参数: stuct in_addr in addr : 只需传入一个结构体变量
返回值: 返回一个字符指针, 否则NULL;
3.3 判断大小端的三种方法
#include <stdio.h>
int main()
{
// 1.指针强转判断大小端
int a = 0x5678;
if (*(char *)&a == 0x78)
printf("小端\n");
else
printf("大端\n");
// 2.类型转换判断大小端
int a = 0x5678;
if ((char)a == 0x78)
printf("小端\n");
else
printf("大端\n");
// 3.共用体判断大小端
union u_data
{
unsigned char a;
unsigned int b;
} data;
data.b = 0x12345678;
if (data.a == 0x78)
{
printf("小端\n");
}
else if (data.a == 0x12)
{
printf("大端\n");
}
return 0;
}
4.TCP编程
C/S:client server 客户端、服务器
B/S:browser server 浏览器与服务器
4.1 流程
服务器:-------《接电话者
1.创建套接字 (socket)-----------------------》有手机
2.指定网络信息--------------------------------》有号码
3.绑定套接字 (bind)--------------------------》绑定手机(插卡)
4.监听 (listen)----------------------------------》待机
5.接收客户端连接请求 (accept)---------》接电话
6.接收发送消息(send recv)-------------------》通话
7.关闭套接字(close)---------------------------》挂断电话
客户端:---------《打电话者
1.创建套接字 (socket)-------------------------》有手机
2.指定(服务器)网络信息-------------------》有对方号码并输入
3.请求连接服务器(connect)------------------》打电话
4.接收发送消息(send recv)-------------------》通话
5.关闭套接字(close)---------------------------》挂断电话
4.2 函数接口
4.2.1 socket
int socket(int domain, int type, int protocol);
功能:创建套接字
参数:
domain:协议族
AF_UNIX, AF_LOCAL 本地通信
AF_INET ipv4
AF_INET6 ipv6
type:套接字类型
SOCK_STREAM:流式套接字
SOCK_DGRAM:数据报套接字
SOCK_RAW:原始套接字
protocol:协议 - 填0 自动匹配底层 ,根据type
系统默认自动帮助匹配对应协议
传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP
网络层:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL)
返回值:
成功 文件描述符
失败 -1,更新errno
4.2.2 bind
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:绑定
参数:
socket:套接字
addr:用于通信结构体 (提供的是通用结构体,需要根据选择通信方式,填充对应结构体-通信当时socket第一个参数确定)
addrlen:结构体大小
返回值:成功 0 失败-1,更新errno
通用结构体:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
ipv4通信结构体:
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
struct in_addr {
uint32_t s_addr;
};
4.2.3 listen
int listen(int sockfd, int backlog);
功能:监听,将主动套接字变为被动套接字
参数:
sockfd:套接字
backlog:同时响应客户端请求链接的最大个数,不能写0.
不同平台可同时链接的数不同,一般写6-8个
(队列1:保存正在连接)
(队列2,连接上的客户端)
返回值:成功 0 失败-1,更新errno
4.2.4 accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept(sockfd,NULL,NULL);
功能:阻塞函数,阻塞等待客户端的连接请求,如果有客户端连接,
则accept()函数返回,返回一个用于通信的套接字文件描述符;
参数:
Sockfd :套接字
addr: 链接客户端的ip和端口号
如果不需要关心具体是哪一个客户端,那么可以填NULL;
addrlen:结构体的大小
如果不需要关心具体是哪一个客户端,那么可以填NULL;
返回值:
成功:文件描述符; //用于通信
失败:-1,更新errno
4.2.5 recv
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能: 接收数据
参数:
sockfd: acceptfd ;
buf 存放位置
len 大小
flags 一般填0,相当于read()函数
MSG_DONTWAIT 非阻塞
返回值:
< 0 失败出错 更新errno
==0 表示客户端退出
>0 成功接收的字节个数
4.2.6 connect
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:用于连接服务器;
参数:
sockfd:socket函数的返回值
addr:填充的结构体是服务器端的;
addrlen:结构体的大小
返回值
-1 失败,更新errno
正确 0
4.2.7 send
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:发送数据
参数:
sockfd:socket函数的返回值
buf:发送内容存放的地址
len:发送内存的长度
flags:如果填0,相当于write();
4.3 初版服务器
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
char buf[128] = {0};
int ret = 0;
// 1.创建流式套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
printf("sockfd:%d\n", sockfd);
// 指定网络信息
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(5677); // 端口
saddr.sin_addr.s_addr = inet_addr("192.168.50.79"); // IP
// 绑定:绑定服务器信息(IP地址\端口号等)
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind err");
return -1;
}
printf("bind okk\n");
// 监听:将主动套接字变为被动套接字
if (listen(sockfd, 8) < 0)
// 队列1:未完成链接队列
// 队列2:已连接队列
{
perror("listen err");
return -1;
}
printf("listen okk\n");
// 阻塞等待链接
int acceptfd = accept(sockfd, NULL, NULL);
// 在TCP服务器中,有两类文件描述符
// 一类用于通信的文件描述符:可能有多个(accept函数返回值)
// 一类用于连接的文件描述符:只有一个(socket函数返回值)
if (acceptfd < 0)
{
perror("acceptfd err");
return -1;
}
printf("acceptfd:%d\n", acceptfd);
// 通信
while (1)
{
// read(acceptfd,buf,sizeof(buf));
ret = recv(acceptfd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("recv err");
return -1;
}
else if (ret == 0)
{
printf("client exit\n");
break;
}
else
printf("buf:%s\n", buf);
memset(buf,0,sizeof(buf));
}
// 关闭文件描述符
close(acceptfd);
close(sockfd);
return 0;
}
4.4 初版客户端
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
char buf[128] = {0};
int ret = 0;
// 1.创建流式套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
printf("sockfd:%d\n", sockfd);
// 指定网络信息
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(5678); // 端口
saddr.sin_addr.s_addr = inet_addr("192.168.50.79"); // IP
// 请求连接服务器
if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("connect err");
return -1;
}
printf("connect okk\n");
// 通信
while (1)
{
// read(acceptfd,buf,sizeof(buf));
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '\0';
send(sockfd, buf, sizeof(buf), 0);
memset(buf, 0, sizeof(buf));
}
close(sockfd);
return 0;
}
4.5 优化
1.优化服务器代码,客户端链接成功后,可以循环多次通信,优化客户端代码,当客户端输入quit时,客户端退出。
2.优化服务器代码客户端输入quit退出后,服务器不退出,等待下一个客户端连接
循环服务器:一个服务器可以有多个客户端连接,但是不能同时连接通信
3.优化客户端代码,地址和端口都通过参数传入
4.自动获取本机地址
5.增加来电显示功能(显示客户端的IP与端口号(accept函数的参数里面))
4.5 优化后服务器
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
char buf[128] = {0};
int ret = 0;
int acceptfd;
// 1.创建流式套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
printf("sockfd:%d\n", sockfd);
// 指定网络信息
struct sockaddr_in saddr,caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1])); // 端口
//saddr.sin_addr.s_addr = inet_addr("192.168.50.79"); // IP
// saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // IP
saddr.sin_addr.s_addr = INADDR_ANY; // IP
int len=sizeof(caddr);
// 绑定:绑定服务器信息(IP地址\端口号等)
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind err");
return -1;
}
printf("bind okk\n");
// 监听:将主动套接字变为被动套接字
if (listen(sockfd, 8) < 0)
// 队列1:未完成链接队列
// 队列2:已连接队列
{
perror("listen err");
return -1;
}
printf("listen okk\n");
while (1)
{
// 阻塞等待链接
acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
// 在TCP服务器中,有两类文件描述符
// 一类用于通信的文件描述符:可能有多个(accept函数返回值)
// 一类用于连接的文件描述符:只有一个(socket函数返回值)
if (acceptfd < 0)
{
perror("acceptfd err");
return -1;
}
printf("acceptfd:%d\n", acceptfd);
printf("ip:%s port:%d\n",inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
// 通信
while (1)
{
// read(acceptfd,buf,sizeof(buf));
ret = recv(acceptfd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("recv err");
return -1;
}
else if (ret == 0)
{
printf("client exit\n");
break;
}
else
printf("buf:%s\n", buf);
memset(buf, 0, sizeof(buf));
}
// 关闭文件描述符
close(acceptfd);
}
close(sockfd);
return 0;
}
4.6 优化后客户端
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
char buf[128] = {0};
int ret = 0;
// 1.创建流式套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
printf("sockfd:%d\n", sockfd);
// 指定网络信息
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[2])); // 端口
saddr.sin_addr.s_addr = inet_addr(argv[1]); // IP
// 请求连接服务器
if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("connect err");
return -1;
}
printf("connect okk\n");
// 通信
while (1)
{
// read(acceptfd,buf,sizeof(buf));
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '\0';
if (!strcmp(buf, "quit"))
break;
send(sockfd, buf, sizeof(buf), 0);
memset(buf, 0, sizeof(buf));
}
close(sockfd);
return 0;
}
4.7 TCP粘包
tcp粘包
tcp拆包
TCP粘包、拆包发生原因:
发生TCP粘包或拆包有很多原因,常见的几点:
1.要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
2.待发送数据大于MSS(传输层的最大报文长度),将进行拆包.
3.要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
4.接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
粘包解决办法:
解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下:
1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
2、发送端将每个数据包封装为固定长度,这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
4、延时发送
4.8 网络协议
4.8 .1 数据的封装与传递过程
思考:
1. 应用层调用send后,是如何把数据发送到另一台机器的某个进程的。
2. 接收的设备收到数据包后,如何处理给应用层?
4.8.2 以太网帧完整帧
● 对于网络层最大数据帧长度是1500字节
● 对于链路层最大数据长度是1518字节(1500+14+CRC)
● 发送时候,IP层协议栈程序检测到发送数据和包头总长度超过1500字节时候,会进行自动分包处理,接收端在IP层进行包重组,然后才继续往上传递
4.9 使用wireshark抓包
4.9.1 启动
win:
双击打开
linux:
sudo wireshark
4.9.2 选择网卡
win:
linux:
4.9.3 过滤包
1. ip.addr == x.x.x.x:只显示源或目标IP地址为x.x.x.x的数据包。
2. tcp.port == x:只显示源或目标端口号为x的TCP数据包。
3. udp.port == x:只显示源或目标端口号为x的UDP数据包。
4. ip.src == x.x.x.x:只显示源IP地址为x.x.x.x的数据包。
5. ip.dst == x.x.x.x:只显目标IP地址为x.x.x.x的数据包。
dst:目标
src:源
seq:序列号
ack:确认号
PSH:数据包
ACK:确认包
SYN:握手包(同步包)
FIN:挥手包
4.9.4 抓包流程
1. 打开win网络调试助手
2. 协议选择tcp server
3. win +r cmd ipconfig找到win的ip
4. 网络调试助手设置IP与端口
5. 打开虚拟机
6. 运行客户端代码
7. 打开wireshark
8. 虚拟机桥接模式:选择wlan网卡。若用nat模式:选择Vmware。。。。
9. 过滤tcp.port==自己设置的端口
10. 网络调试助手与虚拟机客户端连接,发送数据包、关闭连接等都能在wireshark中看到包
4.10 三次握手与四次挥手
4.10.1 三次握手
第一次握手都由客户端发起
在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。
服务器必须准备好接受外来的连接。这通过调用socket、 bind和listen函数来完成,称为被动打开(passive open)。listen
第一次握手:客户通过调用connect进行主动打开(active open)。这引起客户TCP发送一个SYN(表示同步)分节(SYN=J),它告诉服务器客户将在连接中发送数据的初始序列号。并进入SYN_SEND状态,等待服务器的确认。
第二次握手:服务器必须确认客户的SYN,同时自己也得发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器以单个字节向客户发送SYN和对客户SYN的ACK(表示确认),此时服务器进入SYN_RECV状态。
第三次握手:客户收到服务器的SYN+ACK。向服务器发送确认分节,此分节发送完毕,客户服务器进入ESTABLISHED状态,完成三次握手。
1. SYN_SEND:客户端发送SYN报文后进入此状态,等待服务器的确认。
2. SYN_RECV:服务器收到SYN报文后进入此状态,等待客户端的确认。
3. ESTABLISHED:当客户端和服务器端都发送和接收了ACK报文后,连接进入此状态,表示连接已经建立,可以进行数据传输。
客户端的初始序列号为J,而服务器的初始序列号为K。在ACK里的确认号为发送这个ACK的一端所期待的下一个序列号。因为SYN只占一个字节的序列号空间,所以每一个SYN的ACK中的确认号都是相应的初始序列号加1.类似地,每一个FIN(表示结束)的ACK中的确认号为FIN的序列号+1
完成三次握手,客户端与服务器开始传送数据,在上述过程中还有一些重要概念。
未连接队列:在三次握手协议中,服务器维护一个未连接队列,该队列为每个客户端的SYN包(syn=j)开设一个条目,该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客户端确认包。这些条目所标识的连接在服务器处于SYN_RECV状态,当服务器收到客户端确认包时,删除该条目,服务器进入ESTABLISHED状态。
只要发送包就要有发送序号,当发送的包中有ACK时才会有确认号
注意:为什么一定是三次握手,不能是两次握手?
主要是为了防止已经失效的连接请求报文突然又传送到了服务器,从而导致不必要的错误和资源的浪费。两次握手只能保证单向连接是畅通的。因为TCP是一个双向传输协议,只有经过第三次握手,才能确保双向都可以接收到对方的发送的数据。
4.10.2 四次挥手
四次挥手既可以由客户端发起,也可以由服务器发起
TCP连接终止需四个分节。
1MSL:数据包在系统内的最大存活时间
1. 第一次挥手:某个应用进程首先调用close,我们称这一端执行主动关闭。这一端的TCP于是发送一个FIN分节,表示数据发送完毕。
2. 第二次挥手:接收到FIN的另一端执行被动关闭(passive close)。这个FIN由TCP确认。它的接收也作为文件结束符传递给接收端应用进程(放在已排队等候应用进程接收到任何其他数据之后)
3. 第三次挥手:一段时间后,接收到文件结束符的应用进程将调用close关闭它的套接口。这导致它的TCP也发送一个FIN。
4. 第四次挥手:接收到这个FIN的原发送端TCP对它进行确认。
第一次挥手:主动断开方向被动断开方发送FIN挥手包,表示自己发送完毕
第二次挥手:被动断开方接收到FIN之后给主动断开方回复ACK
第三次挥手:被动断开方向主动断开方发送FIN挥手包,表示自己也发送完毕。
第四次挥手:主动断开方接收到FIN之后给被动断开方回复ACK,表示确认关闭连接。
注意:第四次挥手之后主动断开方会等待一段时间再关闭,这个等待的时间是多少?为什么要等待?
等待时间为2MSL,1MSL是报文在系统内的最大存活时间,等待2MSL的时间是为了确保ACK包成功到达被动断开方在第一个MSL时间内,是被动断开方等待主动断开方ACK报文,若没有在1msl的时间内收到,会超时重传FIN报文,主动断开方在剩余的1MSL时间,接收到被动断开方发送的新的FIN
4.11 服务器模型
在网络通信中,通常要求一个服务器连接多个客户端
为了处理多个客户端的请求,通常有多种表现形式
4.11.1 循环服务器模型:一个服务器可以连接多个客户端,但是同一时间智能连接一个处理一个客户端的请求(即前面优化过后的服务器)
4.11.2 并发服务器:一个服务器可以同时处理多个客户端请求
1. 多线程:每有一个客户端连接就创建一个新的线程通信
例子:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
void *handler(void *arg)
{
int ret = 0;
int acceptfd=*(int*)arg;
char buf[128] = {0};
// 通信
while (1)
{
// read(acceptfd,buf,sizeof(buf));
ret = recv(acceptfd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("recv err");
return NULL;
}
else if (ret == 0)
{
printf("client exit\n");
break;
}
else
printf("buf:%s\n", buf);
memset(buf, 0, sizeof(buf));
}
// 关闭文件描述符
close(acceptfd);
}
int main(int argc, char const *argv[])
{
int acceptfd;
pthread_t tid;
// 1.创建流式套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
printf("sockfd:%d\n", sockfd);
// 指定网络信息
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1])); // 端口
// saddr.sin_addr.s_addr = inet_addr("192.168.50.79"); // IP
// saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // IP
saddr.sin_addr.s_addr = INADDR_ANY; // IP
int len = sizeof(caddr);
// 绑定:绑定服务器信息(IP地址\端口号等)
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind err");
return -1;
}
printf("bind okk\n");
// 监听:将主动套接字变为被动套接字
if (listen(sockfd, 8) < 0)
// 队列1:未完成链接队列
// 队列2:已连接队列
{
perror("listen err");
return -1;
}
printf("listen okk\n");
while (1)
{
// 阻塞等待链接
acceptfd = accpt(sockfd, (struct sockaddr *)&caddr, &len);
// 在TCP服务器中,有两类文件描述符
// 一类用于通信的文件描述符:可能有多个(accept函数返回值)
// 一类用于连接的文件描述符:只有一个(socket函数返回值)
if (acceptfd < 0)
{
perror("acceptfd err");
return -1;
}
printf("acceptfd:%d\n", acceptfd);
printf("ip:%s port:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
// 创建线程
pthread_create(&tid, NULL, handler, &acceptfd);
pthread_detach(tid);
//pthread_join();
}
close(sockfd);
return 0;
}
2. 多进程:每有一个客户端连接就创建一个新的进程通信
简略过程:
子进程:通信
父进程:循环等待下个客户端连接
父回收子进程
SIGCHLD:当子进程结束会给父进程发送此信号
父进程就可以捕捉信号并进程子进程回收处理
3. IO多路复用---select
例子:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/select.h>
int main(int argc, char const *argv[])
{
// 1.先构造一张关于文件描述符的表
fd_set rfds, tempfds;
// 2.清空表 FD_ZERO
FD_ZERO(&rfds);
FD_ZERO(&tempfds);
char buf[128] = {0};
int ret = 0;
int acceptfd;
// 1.创建流式套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
printf("sockfd:%d\n", sockfd);
// 指定网络信息
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1])); // 端口
// saddr.sin_addr.s_addr = inet_addr("192.168.50.79"); // IP
// saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // IP
saddr.sin_addr.s_addr = INADDR_ANY; // IP
int len = sizeof(caddr);
// 绑定:绑定服务器信息(IP地址\端口号等)
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind err");
return -1;
}
printf("bind okk\n");
// 监听:将主动套接字变为被动套接字
if (listen(sockfd, 8) < 0)
// 队列1:未完成链接队列
// 队列2:已连接队列
{
perror("listen err");
return -1;
}
printf("listen okk\n");
FD_SET(0, &rfds); // 键盘
FD_SET(sockfd, &rfds); // sockfd
int max = sockfd;
while (1)
{
tempfds = rfds;
// 0
// 4.调用select函数
ret = select(max + 1, &tempfds, NULL, NULL, NULL);
if (ret < 0)
{
perror("select err");
return -1;
}
if (FD_ISSET(0, &tempfds))
{
// 6.做对应的逻辑处理
fgets(buf, sizeof(buf), stdin);
printf("buf:%s\n", buf);
}
if (FD_ISSET(sockfd, &tempfds))
{
// 4 5 6 7
// 阻塞等待链接
acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
// 在TCP服务器中,有两类文件描述符
// 一类用于通信的文件描述符:可能有多个(accept函数返回值)
// 一类用于连接的文件描述符:只有一个(socket函数返回值)
if (acceptfd < 0)
{
perror("acceptfd err");
return -1;
}
printf("acceptfd:%d\n", acceptfd);
printf("ip:%s port:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
FD_SET(acceptfd, &rfds);
if (acceptfd > max)
max = acceptfd;
}
for (int i = sockfd + 1; i <= max; i++)
{
if (FD_ISSET(i, &tempfds))
{
// // 通信
// // read(acceptfd,buf,sizeof(buf));
ret = recv(i, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("recv err");
return -1;
}
else if (ret == 0)
{
printf("client exit\n");
close(i);
// 从原表中删除
FD_CLR(i, &rfds);
// 3 4 5
while(!FD_ISSET(max,&rfds))
max--;
}
else
printf("buf:%s\n", buf);
// 关闭文件描述符
// close(acceptfd);
}
}
memset(buf, 0, sizeof(buf));
}
close(sockfd);
return 0;
}
如果发现问题请私信联系,谢谢。