1.理论基础
1.1 对等通信
![输入图片说明]
![](https://img.haomeiwen.com/i1492538/9491940218e81979.png)
1.2 数据封装
![输入图片说明]
![](https://img.haomeiwen.com/i1492538/6aba791658276172.png)
1.3 tcp的三次握手和四次断开
![](https://img.haomeiwen.com/i1492538/9d3283b4cdfe34fe.png)
![](https://img.haomeiwen.com/i1492538/697a5ed2657534cf.png)
TCP如何保证可靠性
- 应用数据被分割成TCP认为最适合发送的数据块,称为段传递给IP层。
- 当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
- 当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒。
- TCP将保持它首部和数据的校验和。这是一个端到端的校验和,目的是检测数据在传输过程中的任何变化。如果收到段的校验和有差错,TCP将丢弃这个报文段并且不确认(导致对方超时重传)
- TCP承载于IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。TCP将对收到的数据进行重新排序。
- IP数据报会发生重复,TCP的接收端必须丢弃重复的数据。
- TCP还能提供流量控制。TCP连接的每一方都有一定大小的缓冲空间。
1.4 数据在网络中的传输过程
![](https://img.haomeiwen.com/i1492538/7f8991ffd84d6757.png)
步骤a:应用程序ping会判断发送的是主机名还是IP地址,调用函数gethostbyname()解析主机机B,将主机名转换成一个32位的IP地址。这个过程叫做DNS域名解析
步骤b:ping程序向目的IP地址发送一个ICMP的ECHO包
步骤c:将目标主机的IP地址转换为48位硬件地址,在局域网内发送ARP请求广播,查找主机B的硬件地址。
步骤d:主机B的ARP协议层接收到主机A的ARP请求后,将本机的硬件地址填充到应答包,发送ARP应答到主机A。
步骤e:发送ICMP数据包到主机B
步骤f:主机B接收到主机A的ICMP包,发送响应包。
步骤g:主机A接收到主机B的ICMP包响应包。
2.套接字地址结构
2.1 ipv4套接字
struct sockaddr_in {
uint8_t sin_len; 4
sa_family_t sin_family; 4
in_port_t sin_port; 2
struct in_addr sin_addr; 4
char sin_zero[8]; 8
};
sin_len:整个sockaddr_in结构体的长度,在4.3BSD-Reno版本之前的第一个成员是sin_family.
sin_family:指定该地址家族,在这里必须设为AF_INET
sin_port:端口
sin_addr:IPv4的地址;
sin_zero:暂不使用,一般将其设置为0
2.2 通用套接字
struct sockaddr {
uint8_t sin_len;
sa_family_t sin_family;
char sa_data[14]; //14
};
sin_len:整个sockaddr结构体的长度
sin_family:指定该地址家族
sa_data:由sin_family决定它的形式
3.字节序问题
- 大端字节序(Big Endian)
最高有效位(MSB:Most Significant Bit)存储于最低内存地址处,最低有效位(LSB:Lowest Significant Bit)存储于最高内存地址处。 - 小端字节序(Little Endian)
最高有效位(MSB:Most Significant Bit)存储于最高内存地址 处,最低有效位(LSB:Lowest Significant Bit)存储于最低内存地址处。 - 主机字节序
不同的主机有不同的字节序,如x86为小端字节序,Motorola 6800为大端字节序,ARM字节序是可配置的。 - 网络字节序
网络字节序规定为大端字节序
字节序转换函数
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
说明:在上述的函数中,h代表host;n代表network s代表short;l代表long
4.地址转换函数
//输入参数string包含ASCII表示的IP地址。
//输出参数addr是将要用新的IP地址更新的结构。
int inet_aton(const char *cp, struct in_addr *inp);
//参数:一个网络上的IP地址
//返回:如果正确,返回一个字符指针,指向一块存储着点分格式IP地址的静态缓冲区,错误返回null
char *inet_ntoa(struct in_addr in);
//若字符串有效则将字符串转换为32位二进制网络字节序的IPV4地址
in_addr_t inet_addr(const char *cp);
5.套接字类型
- 流式套接字(SOCK_STREAM)
提供面向连接的、可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收。 - 数据报式套接字(SOCK_DGRAM)
提供无连接服务。不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。 - 原始套接字(SOCK_RAW)
6.tcp编程
6.1 tcp服务端/客户端模型
![](https://img.haomeiwen.com/i1492538/3bf391ff76b69266.png)
6.2 服务端api
6.2.1 socket函数
包含头文件<sys/socket.h>
功能:创建一个套接字用于通信
原型
int socket(int domain, int type, int protocol);
参数
domain :指定通信协议族(protocol family)
type:指定socket类型,流式套接字SOCK_STREAM,数据报套接字SOCK_DGRAM,原始套接字SOCK_RAW
protocol :协议类型
返回值:成功返回非负整数, 它与文件描述符类似,我们把它称为套接口描述字,简称套接字。失败返回-1
int sockfd = 0;
//创建socket
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("fun socket\n");
exit(0);
}
6.2.2 bind
包含头文件<sys/socket.h>
功能:绑定一个本地地址到套接字
原型
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数
sockfd:socket函数返回的套接字
addr:要绑定的地址
addrlen:地址长度
返回值:成功返回0,失败返回-1
//定义socket结构体
struct sockaddr_in srvaddr;
//设置协议族
srvaddr.sin_family = AF_INET;
//设置端口
srvaddr.sin_port = htons(8001);
//增加ip地址
srvaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //127.0.0.1
//绑定IP
if (bind(sockfd, (struct sockaddr *) &srvaddr, sizeof(srvaddr)) < 0)
{
perror("fun bind\n");
exit(0);
}
6.2.3 listen
一但调用listen函数,这个套接字sockfd将变成被动套接字;只能接受连接,不能主动的发送连接
队列由内核管理,一部分是完成三次握手的,一部分是没有完成三次握手的。
对于给定的监听套接口,内核要维护两个队列:
1、已由客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手过程
2、已完成连接的队列
![](https://img.haomeiwen.com/i1492538/ae3e591a20a9bd24.png)
![](https://img.haomeiwen.com/i1492538/152e941ca37137ab.png)
if (listen(sockfd, SOMAXCONN) < 0)
{
perror("fun listen\n");
exit(0);
}
6.2.4 accept
包含头文件<sys/socket.h>
功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞。
原型
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数
sockfd:服务器套接字
addr:将返回对等方的套接字地址
addrlen:返回对等方的套接字地址长度
返回值:成功返回非负整数,失败返回-1
unsigned int conn = 0;
//accept接受已经完成三次握手的链接,没有链接会阻塞直到有链接
conn = accept(sockfd, (struct sockaddr *) &peeraddr,
(socklen_t *) &peerlen);
if (conn == -1)
{
perror("fun listen\n");
exit(0);
}
printf("perradd:%s\n perrport:%d\n", inet_ntoa(peeraddr.sin_addr),
ntohs(peeraddr.sin_port));
6.3 客户端api
6.3.1 connect
包含头文件<sys/socket.h>
功能:建立一个连接至addr所指定的套接字
原型
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数
sockfd:未连接套接字
addr:要连接的套接字地址
addrlen:第二个参数addr长度
返回值:成功返回0,失败返回-1
//设置客服端链接的tcpip结构体信息
struct sockaddr_in srvaddr;
//协议族
srvaddr.sin_family = AF_INET;
//设置端口
srvaddr.sin_port = htons(8001);
//设置ip
srvaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //127.0.0.1
//客服端链接
if (connect(sockfd, (struct sockaddr*) (&srvaddr), sizeof(srvaddr)) < 0)
{
perror("fun socket\n");
exit(0);
}
6.4 示例
6.4.1 服务端
int main()
{
int sockfd = 0;
//创建socket
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("fun socket\n");
exit(0);
}
//定义socket结构体
struct sockaddr_in srvaddr;
//设置协议族
srvaddr.sin_family = AF_INET;
//设置端口
srvaddr.sin_port = htons(8001);
//增加ip地址
srvaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //127.0.0.1
//srvaddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址
//绑定IP
if (bind(sockfd, (struct sockaddr *) &srvaddr, sizeof(srvaddr)) < 0)
{
perror("fun bind\n");
exit(0);
}
//一但调用listen函数,这个套接字sockfd将变成被动套接字;只能接受连接,不能主动的发送连接
//listen 做了两个队列。。。。。。
// 队列由内核管理,一部分是完成三次握手的,一部分是没有完成三次握手的。
if (listen(sockfd, SOMAXCONN) < 0)
{
perror("fun listen\n");
exit(0);
}
//struct sockaddr peeraddr; //通用ip
//socklen_t perrlen ;
struct sockaddr_in peeraddr; //tcpip地址结构
socklen_t peerlen = sizeof(peeraddr);
unsigned int conn = 0;
//accept接受已经完成三次握手的链接,没有链接会阻塞直到有链接
conn = accept(sockfd, (struct sockaddr *) &peeraddr,
(socklen_t *) &peerlen);
if (conn == -1)
{
perror("fun listen\n");
exit(0);
}
printf("perradd:%s\n perrport:%d\n", inet_ntoa(peeraddr.sin_addr),
ntohs(peeraddr.sin_port));
char revbuf[1024] = { 0 };
while (1)
{
int ret = read(conn, revbuf, sizeof(revbuf));
if (ret == 0)
{
//如果在读的过程中,对方已经关闭,tcpip协议会返回一个0数据包
printf("对方已关闭\n");
exit(0);
} else if (ret < 0)
{
perror("读数据失败\n");
exit(0);
}
//ssize_t write(int fd, const void *buf, size_t count);
fputs(revbuf, stdout); //服务器端收到数据,打印屏幕
write(conn, revbuf, ret); //服务器端回发报文
}
return 0;
}
6.4.2 客户端
int main()
{
int sockfd = 0;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("fun socket\n");
exit(0);
}
//设置客服端链接的tcpip结构体信息
struct sockaddr_in srvaddr;
//协议族
srvaddr.sin_family = AF_INET;
//设置端口
srvaddr.sin_port = htons(8001);
//设置ip
srvaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //127.0.0.1
//srvaddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址
//客服端链接
if (connect(sockfd, (struct sockaddr*) (&srvaddr), sizeof(srvaddr)) < 0)
{
perror("fun socket\n");
exit(0);
}
//设置接受和发送的缓存。
char revbuf[1024] = { 0 };
char sendbuf[1024] = { 0 };
//
//char *fgets(char *s, int size, FILE *stream); 从stream 读取size-1大小的数据存入s,最后加'\0'
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
//向服务写数据
//ssize_t write(int fd, const void *buf, size_t count);
// 从buf中读取count大小的数据存入文件描述符为fd的文件中。
write(sockfd, sendbuf, strlen(sendbuf)); //服务器端回发信息
//从服务器读数据
//ssize_t read(int fd, void *buf, size_t count);
//从文件描述符为fd的文件中读取大小为count的数据存入buf中。
read(sockfd, revbuf, sizeof(revbuf));
fputs(revbuf, stdout); //从服务器收到数据,打印屏幕
memset(revbuf, 0, sizeof(revbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
close(sockfd);
return 0;
}
7.地址复用
TCP,先调用close()的一方会进入TIME_WAIT状态
一般来说,一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用。
SO_REUSEADDR用于对TCP套接字处于TIME_WAIT状态下的socket,才可以重复绑定使用。
server程序总是应该在调用bind()之前设置SO_REUSEADDR套接字选项。
8.并发服务器框架
int main()
{
int sockfd = 0;
//创建socket TCP 默认协议
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("fun socket\n");
exit(0);
}
//TCP/IP 协议的结构体
struct sockaddr_in srvaddr;
//地址协议
srvaddr.sin_family = AF_INET;
//端口
srvaddr.sin_port = htons(8001);
//地址
srvaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //127.0.0.1
int optval = 1;
//设置地址复用
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval))
< 0)
{
perror("setsockopt bind\n");
exit(0);
}
//绑定
if (bind(sockfd, (struct sockaddr *) &srvaddr, sizeof(srvaddr)) < 0)
{
perror("fun bind\n");
exit(0);
}
//一但调用listen函数,这个套接字sockfd将变成被动套接字;只能接受连接,不能主动的发送连接
//listen 做了两个队列。。。。。。
if (listen(sockfd, SOMAXCONN) < 0)
{
perror("fun listen\n");
exit(0);
}
//struct sockaddr peeraddr; //通用ip
//socklen_t perrlen ;
struct sockaddr_in peeraddr; //tcpip地址结构
socklen_t peerlen = sizeof(peeraddr);
unsigned int conn = 0;
//accept 返回一个新的连接 ,这个新的连接是一个主动套接字
while (1)
{
conn = accept(sockfd, (struct sockaddr *) &peeraddr,
(socklen_t *) &peerlen);
if (conn == -1)
{
perror("fun listen\n");
exit(0);
}
printf("perradd:%s\n perrport:%d\n", inet_ntoa(peeraddr.sin_addr),
ntohs(peeraddr.sin_port));
int pid = fork();
if (pid == 0)
{
close(sockfd); //子进程不需要侦听,
char revbuf[1024] = { 0 };
while (1)
{
int ret = read(conn, revbuf, sizeof(revbuf));
if (ret == 0)
{
//如果在读的过程中,对方已经关闭,tcpip协议会返回一个0数据包
printf("对方已关闭\n");
exit(0);
} else if (ret < 0)
{
perror("读数据失败\n");
exit(0);
}
//ssize_t write(int fd, const void *buf, size_t count);
fputs(revbuf, stdout); //服务器端收到数据,打印屏幕
write(conn, revbuf, ret); //服务器端回发信息
}
} else if (pid > 0)
{
close(conn); //父进程不需要conn
} else
{
close(conn);
//close(sockfd);
}
}
close(conn);
close(sockfd);
return 0;
}
9.点对点聊天程序
![](https://img.haomeiwen.com/i1492538/dd94bc3eff5d8fb9.png)
9.1 服务端
void handle(int num)
{
printf("recv num:%d \n",num);
exit(0);
}
int main()
{
int sockfd = 0;
//信号处理函数
signal(SIGUSR1, handle);
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("fun socket\n");
exit(0);
}
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(8001);
srvaddr.sin_addr.s_addr = inet_addr("192.168.6.249"); //127.0.0.1
//地址复用
int optval = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
{
perror("setsockopt bind\n");
exit(0);
}
if ( bind( sockfd, (struct sockaddr *)&srvaddr, sizeof(srvaddr)) < 0)
{
perror("fun bind\n");
exit(0);
}
//一但调用listen函数,这个套接字sockfd将变成被动套接字;只能接受连接,不能主动的发送连接
//listen 做了两个队列。。。。。。
if ( listen(sockfd, SOMAXCONN)<0 )
{
perror("fun listen\n");
exit(0);
}
//struct sockaddr peeraddr; //通用ip
//socklen_t perrlen ;
struct sockaddr_in peeraddr; //tcpip地址结构
socklen_t peerlen = sizeof(peeraddr);
unsigned int conn = 0;
//accept 返回一个新的连接 ,这个新的连接是一个主动套接字
conn = accept(sockfd, (struct sockaddr *)&peeraddr, (socklen_t *)&peerlen);
if (conn == -1)
{
perror("fun listen\n");
exit(0);
}
printf("perradd:%s\n perrport:%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
int pid;
pid = fork();
if (pid > 0)
{
char revbuf[1024] = {0};
while (1)
{
int ret = read(conn, revbuf, sizeof(revbuf));
if (ret == 0)
{
//如果在读的过程中,对方已经关闭,tcpip协议会返回一个0数据包
printf("对方已关闭\n");
break;
}
else if (ret < 0)
{
perror("读数据失败\n");
exit(0);
}
//ssize_t write(int fd, const void *buf, size_t count);
fputs(revbuf, stdout); //服务器端收到数据,打印屏幕
memset(revbuf, 0, sizeof(revbuf));
}
close(conn);
//kill(pid , SIGUSR1);
sleep(1);
}
else
{
//char revbuf[1024] = {0};
char sendbuf[1024] = {0};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
//向客户端写数据
write(conn, sendbuf, strlen(sendbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
}
close(sockfd);
return 0;
}
9.2 客户端
void handle(int num)
{
printf("recv num:%d \n",num);
exit(0);
}
int main()
{
int sockfd = 0;
signal(SIGUSR1, handle);
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("fun socket\n");
exit(0);
}
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(8001);
srvaddr.sin_addr.s_addr = inet_addr("192.168.6.249"); //127.0.0.1
if ( connect(sockfd, (struct sockaddr*) (&srvaddr), sizeof(srvaddr)) < 0)
{
printf("errno:%d \n", errno);
perror("fun socket\n");
exit(0);
}
int pid = 0;
pid = fork();
if (pid > 0) //父进程
{
//char revbuf[1024] = {0};
char sendbuf[1024] = {0};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
//向服务写数据
write(sockfd, sendbuf, strlen(sendbuf)); //服务器端回发信息
memset(sendbuf, 0, sizeof(sendbuf));
}
}
else
{
char revbuf[1024] = {0};
while(1)
{
int ret = 0;
//从服务器读数据
ret = read(sockfd, revbuf, sizeof(revbuf));
if (ret < 0)
{
printf("read err\n");
break;
}
if (ret == 0)
{
printf("read err\n");
break;
}
//
fputs(revbuf, stdout); //从服务器收到数据,打印屏幕
memset(revbuf, 0, sizeof(revbuf));
}
close(sockfd);
kill(getppid(), SIGUSR1);
exit(0);
}
close(sockfd);
return 0;
}
10.socket理论进阶
10.1 tcp是一种流协议
tcp是一种流协议,也就是说数据是以字节流的形式传递给接收者的,没有固定的报文或报文边界的概念,读取tcp数据是无法预先知道在一次读调用中会返回多少字节的。
![](https://img.haomeiwen.com/i1492538/672fd2f1ca179382.png)
主机A上的应用程序向主机B发报文,假设主机A有两条报文要发送,并两次调用send来发送,每条报文调用一次send,理想的情况是两个报文时两个独立体,在各自的分组中发送,但是在实际的传输过程中,send通常只是将数据复制到A的tcp/ip栈中就返回了,由tcp决定发送多少数据,这种决定的过程和因素很多,所以数据的发送可能有多种可能。也就是说tcp不一定将一条报文的全部内容都放在一个分组中传送出去
现在从B的角度来看,B在recv的时候也不会对tcp发给他的内容做假设,这时会出现以下几种情况:
- 没有数据可读,
- 获取到了M1的一部分
- 获取到了M1的全部
- 获取到了M1的全部以及M2的一部分,等等情况
对比
-
TCP(传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法,将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
-
UDP(用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,
由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
10.2 粘包
TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
两种情况会造成粘包
1.发送端的原因
TCP默认会使用Nagle算法。而Nagle算法主要做两件事:1)只有上一个分组得到确认,才会发送下一个分组;2)收集多个小分组,在一个确认到来时一起发送
2.接收方不及时接收缓冲区的包,造成多个包接收:
TCP接收到分组时,并不会立刻送至应用层处理,或者说,应用层并不一定会立即处理;实际上,TCP将收到的分组保存至接收缓存里,然后应用程序主动从缓存里读收到的分组。这样一来,如果TCP接收分组的速度大于应用程序读分组的速度,多个包就会被存至缓存,应用程序读时,就会读到多个首尾相接粘到一起的包
10.2.2 不需要处理粘包的几种情况
连续的数据流不需要处理。如一个在线视频,它是一个连续不断的流, 不需要考虑分包。
每发一个消息,建一次连接的情况。
发送端使用了TCP强制数据立即传送的操作指令push。
UDP,UDP不需要处理。
10.2.3 处理方式
1)格式化数据:每条数据有固定的格式(开始符、结束符),这种方法简单易行,但选择开始符和结束符的时候一定要注意每条数据的内部一定不能出现开始符或结束符;
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count;
ssize_t nread;
char *bufp = (char*) buf;
while (nleft > 0)
{
if ((nread = read(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
continue;
return -1;
} else if (nread == 0)
return count - nleft;
bufp += nread;
nleft -= nread;
}
return count;
}
ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;//需要写入的数据个数
ssize_t nwritten;//
char *bufp = (char*) buf;
while (nleft > 0)
{
if ((nwritten = write(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
continue;
return -1;
} else if (nwritten == 0)
continue;
bufp += nwritten;
nleft -= nwritten;
}
return count;
}
//从指定的socket中读取指定大小的数据但 不取出,封装后不被信号中断
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while (1)
{
//MSG_PEEK 读取队列中指定大小的数据,但不取出
int ret = recv(sockfd, buf, len, MSG_PEEK);
//如果被信号中断,则继续
if (ret == -1 && errno == EINTR)
continue;
return ret;
}
}
//maxline 一行最大数
//先提前peek一下缓冲区,如果有数据从缓冲区的读数据,
//1、缓冲区数据中带\n
//2 缓存区中不带\n
//读取读取包直到\n
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread;//成功预读取的数据的个数
char *bufp = buf;//读取数据存放的数组,在外分配内存
int nleft = maxline;//封包最大值
while (1)
{
//看一下缓冲区有没有数据,并不移除内核缓冲区数据
ret = recv_peek(sockfd, bufp, nleft);
if (ret < 0) //失败
return ret;
else if (ret == 0) //对方已关闭
return ret;
nread = ret;
int i;
for (i = 0; i < nread; i++)
{
if (bufp[i] == '\n') //若缓冲区有\n
{
ret = readn(sockfd, bufp, i + 1); //读走数据
if (ret != i + 1)
exit(EXIT_FAILURE);
return ret; //有\n就返回,并返回读走的数据
}
}
if (nread > nleft) //如果读到的数大于 一行最大数 异常处理
exit(EXIT_FAILURE);
nleft -= nread; ////若缓冲区没有\n, 把剩余的数据读走
ret = readn(sockfd, bufp, nread);
if (ret != nread)
exit(EXIT_FAILURE);
bufp += nread; //bufp指针后移后,再接着偷看缓冲区数据recv_peek,直到遇到\n
}
return -1;
}
void do_service(int conn)
{
char recvbuf[1024];
while (1)
{
memset(recvbuf, 0, sizeof(recvbuf));
int ret = readline(conn, recvbuf, 1024);
if (ret == -1)
ERR_EXIT("readline");
if (ret == 0)
{
printf("client close\n");
break;
}
fputs(recvbuf, stdout);
//将读到的数据再发送,发送没有对\n处理
writen(conn, recvbuf, strlen(recvbuf));
}
}
int main(void)
{
int listenfd;
if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/* if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8001);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
/*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
/*inet_aton("127.0.0.1", &servaddr.sin_addr);*/
int on = 1;
//设置socket 地址复用
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
ERR_EXIT("setsockopt");
if (bind(listenfd, (struct sockaddr*) &servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("bind");
if (listen(listenfd, SOMAXCONN) < 0)
ERR_EXIT("listen");
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
int conn;
pid_t pid;
while (1)
{
if ((conn = accept(listenfd, (struct sockaddr*) &peeraddr, &peerlen))
< 0)
ERR_EXIT("accept");
printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),
ntohs(peeraddr.sin_port));
pid = fork();
if (pid == -1)
ERR_EXIT("fork");
if (pid == 0)
{
close(listenfd);
do_service(conn);
exit(EXIT_SUCCESS);
} else
close(conn);
}
return 0;
}
2)发送长度:发送每条数据的时候,将数据的长度一并发送,比如可以选择每条数据的前4位是数据的长度,应用层处理时可以根据长度来判断每条数据的开始和结束。
server
//自定义封包的结构体
struct packet
{
int len; //自定义的包头,包含了包的大小
char buf[1024]; //包存放的数据
};
//1一次全部读走 //2次读完数据 //出错分析 //对方已关闭
//思想:tcpip是流协议,不能保证1次读操作,能全部把报文读走,所以要循环读指定长度的数据。
//按照count大小读数据,
//若读取的长度ssize_t<count 说明读到了一个结束符,对方已关闭。
//@ssize_t:返回读的长度 若ssize_t<count 读失败失败
//@buf:接受数据内存首地址
//@count:接受数据长度
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; //定义剩余没有读取的个数
ssize_t nread; //读取的个数
char *bufp = (char*) buf; //将参数接过来
while (nleft > 0) //当剩余需要读取的个数>0
{
//成功读取的个数小于0,则判断出错的原因
if ((nread = read(fd, bufp, nleft)) < 0)
{
//如果errno被设置为EINTR为被信号中断,如果是被信号中断继续,
//不是信号中断则退出。
if (errno == EINTR)
continue;
return -1;
} else if (nread == 0) //若对方已关闭
return count - nleft;
bufp += nread; //将 字符串指针向后移动已经成功读取个数的大小。
nleft -= nread; //需要读取的个数=需要读取的个数-以及成功读取的个数
}
return count;
}
//1一次全部读走 //2次读完数据 //出错分析 //对方已关闭
//思想:tcpip是流协议,不能1次把指定长度数据,全部写完
//按照count大小写数据
//若读取的长度ssize_t<count 说明读到了一个结束符,对方已关闭。
//@ssize_t:返回写的长度 -1失败
//@buf:待写数据首地址
//@count:待写长度
ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count; //需要写入的个数
ssize_t nwritten; //已经成功写入的个数
char *bufp = (char*) buf; //接参数
while (nleft > 0) //如果需要写入的个数>0
{
//如果写入成功的个数<0 判断是否是被信号打断
if ((nwritten = write(fd, bufp, nleft)) < 0)
{
//信号打断,则继续
if (errno == EINTR)
continue;
return -1;
}
//需要写入的数据个数>0
//如果成功写入的个数为0 则继续
else if (nwritten == 0)
continue;
//将bufp指针向后移动已经
bufp += nwritten;
//剩余个数
nleft -= nwritten;
}
return count;
}
void do_service(int conn)
{
//定义了自定封包结构体
struct packet recvbuf;
//
int n;
while (1)
{
//清空结构体
memset(&recvbuf, 0, sizeof(recvbuf));
//读取包的头4字节
int ret = readn(conn, &recvbuf.len, 4); //读包头 4个字节
if (ret == -1)
ERR_EXIT("read");
//如果读取的个数小于4,则客服端已经关闭
else if (ret < 4)
{
printf("client close\n");
break;
}
//将网络数据转换为本地数据结构,比如网络数据为大端,而本地数据为小端
n = ntohl(recvbuf.len);
//根据包头里包含的大小读取数据
ret = readn(conn, recvbuf.buf, n); //根据长度读数据
if (ret == -1)
ERR_EXIT("read");
//如果读取的数据的大小小于封包包头中包的大小,那么客服端已经关闭
else if (ret < n)
{
printf("client close\n");
break;
}
//将数据打印出。
fputs(recvbuf.buf, stdout);
//将接受到的数据再直接发出去。
writen(conn, &recvbuf, 4 + n); //注意写数据的时候,多加4个字节
}
}
int main(void)
{
int listenfd; //定义了监听socket的文件描述符
//创建socket TCP 协议
if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/* if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8001);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
/*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
/*inet_aton("127.0.0.1", &servaddr.sin_addr);*/
int on = 1;
//设置地址复用
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
ERR_EXIT("setsockopt");
//绑定
if (bind(listenfd, (struct sockaddr*) &servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("bind");
//监听
if (listen(listenfd, SOMAXCONN) < 0)
ERR_EXIT("listen");
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
int conn;
pid_t pid;
while (1)
{
//接受已经链接的socket
if ((conn = accept(listenfd, (struct sockaddr*) &peeraddr, &peerlen))
< 0)
ERR_EXIT("accept");
printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),
ntohs(peeraddr.sin_port));
//新建子进程
pid = fork();
if (pid == -1)
ERR_EXIT("fork");
if (pid == 0)
{ //子进程
//子进程不需要监听socket,关闭监听的socket
close(listenfd);
do_service(conn);
exit(EXIT_SUCCESS);
} else
close(conn);
}
return 0;
}
client
//定义自定义封装包
struct packet
{
int len;
char buf[1024];
};
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; //剩下需要读取的数据的个数
ssize_t nread; //成功读取的个数
char *bufp = (char*) buf;
while (nleft > 0)
{
if ((nread = read(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
continue;
return -1;
} else if (nread == 0)
return count - nleft;
bufp += nread;
nleft -= nread;
}
return count;
}
ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char *bufp = (char*) buf;
while (nleft > 0)
{
if ((nwritten = write(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
continue;
return -1;
} else if (nwritten == 0)
continue;
bufp += nwritten;
nleft -= nwritten;
}
return count;
}
int main(void)
{
int sock;
//定义socket
if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
ERR_EXIT("socket");
// 定义结构体
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8001);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
/*int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
*/
if (connect(sock, (struct sockaddr*) &servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("connect");
struct packet sendbuf;
struct packet recvbuf;
memset(&sendbuf, 0, sizeof(sendbuf));
memset(&recvbuf, 0, sizeof(recvbuf));
int n;
/*
* char *fgets(char *s, int size, FILE *stream);
* */
while (fgets(sendbuf.buf, sizeof(sendbuf.buf), stdin) != NULL)
{
//获取字符串的大小
n = strlen(sendbuf.buf);
//将n转换为网络字节存储
sendbuf.len = htonl(n);
//将获取的包发送
writen(sock, &sendbuf, 4 + n);
int ret = readn(sock, &recvbuf.len, 4);
if (ret == -1)
ERR_EXIT("read");
else if (ret < 4)
{
printf("client close\n");
break;
}
//将转换为本地字节存储
n = ntohl(recvbuf.len);
ret = readn(sock, recvbuf.buf, n);
if (ret == -1)
ERR_EXIT("read");
else if (ret < n)
{
printf("client close\n");
break;
}
fputs(recvbuf.buf, stdout);
memset(&sendbuf, 0, sizeof(sendbuf));
memset(&recvbuf, 0, sizeof(recvbuf));
}
close(sock);
return 0;
}
11。长连接和短连接
指的是客户端
短连接:每次都建立连接,通信完成之后就断开
长连接:建立一次连接,然后就不断开了
//长连接
int main01()
{
int sockfd = 0;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("fun socket\n");
exit(0);
}
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(8001);
srvaddr.sin_addr.s_addr = inet_addr("192.168.6.249"); //127.0.0.1
//srvaddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址
if (connect(sockfd, (struct sockaddr*) (&srvaddr), sizeof(srvaddr)) < 0)
{
printf("errno:%d \n", errno);
perror("fun socket\n");
exit(0);
}
char revbuf[1024] = { 0 };
char sendbuf[1024] = { 0 };
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
//向服务写数据
write(sockfd, sendbuf, strlen(sendbuf)); //服务器端回发信息
//从服务器读数据
read(sockfd, revbuf, sizeof(revbuf));
//
fputs(revbuf, stdout); //从服务器收到数据,打印屏幕
memset(revbuf, 0, sizeof(revbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
close(sockfd);
return 0;
}
//短连接
int main()
{
int i = 0;
//创建多个socket发送信息
for (i = 0; i < 10; i++)
{
int sockfd = 0;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("fun socket\n");
exit(0);
}
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(8001);
srvaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //127.0.0.1
//srvaddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址
if (connect(sockfd, (struct sockaddr*) (&srvaddr), sizeof(srvaddr)) < 0)
{
printf("errno:%d \n", errno);
perror("fun socket\n");
exit(0);
}
char revbuf[1024] = { 0 };
char sendbuf[1024] = { 0 };
{
sprintf(sendbuf, "i:%d\n", i);
//向服务写数据
write(sockfd, sendbuf, strlen(sendbuf)); //服务器端回发信息
//从服务器读数据
read(sockfd, revbuf, sizeof(revbuf));
//
fputs(revbuf, stdout); //从服务器收到数据,打印屏幕
memset(revbuf, 0, sizeof(revbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
close(sockfd);
}
return 0;
}
网友评论