一.TCP三次握手,四次挥手
1.TCP固定报头结构
在上述图中可以看到,头部长度为4位即长度大小为0~15,即最多有15行的信息,一行是32位,即4字节,即整个报头最大长度60字节。由图看可知固定部分有5行,占了20字节。
2.三次握手
在客户端进行到connect()建立连接后进行握手,客户端首先给服务端发送一个TCP报文(头部结构),此时32位序号是有效的,32位确认号此时无效,此时SYN标志为1。(下面序号用seq表示) 三次握手在传输层底层自己实现。
3.四次挥手(当进行close后进行挥手,客户端close两次,服务端close两次,总共四次)断开连接
4.挥手可不可以是三次:是可以的,在TCP头部结构中本来就存在32位序号和32位序列号,所以可以将第二步与第三步合并起来服务端可以同时将确认号和序号一起发送过去,此时就变成了三次挥手。(前提是服务端必须也执行了close操作)
5.TCP保证可靠性方法:
TCP是传输层的协议,在网络层要交给IP,IP是不可靠的,故TCP的可靠性就是靠应答确认,超时重传机制(就是收到一个数据,就必须发送一个报文说收到了,这样才会发送下一个数据),这个确认是TCP底层自己确认完成的。
TCP还有乱序重排,去重机制(会给每个数据编号,因为不能保证先走的数据先到,这样在接收的时候会根据编号重新排列。去重就是当发送数据后,返回的确认报文在路上丢失了,服务端就会认为没有发送过去,就会重新发送,这样就会产生重复的。)
TCP还有滑动窗口进行流量控制(发送数据时会跟对方进行协商,每次发送固定的数据,下一次发送会接着上次发的地方)
6.TCP的特点:面向连接(握手,挥手),可靠的(上述保证可靠的三种方法),流式服务(就是一次发送的数据可能被对方多次收到,多次发送的数据可能被对方一次收到,具体见上一篇recv的介绍)
7.TCP的粘包:两次或多次发送的数据被对方一次recv收到就是粘包。如:客户端逐字母发送“hello”,服务端逐字母收到后返回的5次ok被客户端一次收到。
粘包的影响:在不同情况下影响不同,如要接收文件时,客户端发送多次文件,服务端一次收到和多次收到,影响不大。 但如客户端发送的数据是某个物体的长宽高分为三次发送,但要是服务端一次收到,就会在服务端的其余recv阻塞住。
解决粘包的方法:①用【7】【3】【2】将各个数据分隔开,在书写代码中不将循环写死,每次遇到【】就是一次数据。
②每次发送数据之前设置报头,遇见一次报头就是一次数据的发送。
二.多线程并发
实现多个客户端可以同时连接上服务端,并且可以给服务端发送信息,运用线程的方法,(在上一篇中运用了进程的方法)具体就是将发送信息(send),接收信息(recv),关闭连接(close)放在线程中实现。
①服务端:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
int socket_init();//函数声明
void* fun(void* arg)
{
int *p = (int*)arg;
int c = *p;
free(p);
while( 1 )
{
char buff[128] = {0};
int n = recv(c,buff,127,0);//阻塞 , -1, 0 对方关闭了
if ( n <= 0 )
{
break;
}
printf("recv:%s\n",buff);
send(c,"ok",2,0);
}
close(c);
printf("client close\n");
}
int main()
{
int sockfd = socket_init();//3
if ( sockfd == -1 )
{
exit(1);
}
while( 1 )
{
struct sockaddr_in caddr;//记录客户端地址ip ,port
int len = sizeof(caddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);//caddr存放客户端Ip,port, 阻塞 文件描述符为4
if( c < 0 )//4
{
continue;
}
printf("accept c=%d\n",c);
int *p = (int*)malloc(sizeof(int));
if ( p == NULL )
{
close(c);
continue;
}
*p = c;
pthread_t id;
pthread_create(&id,NULL,fun,(void*)p);
}
}
int socket_init()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);//TCP,创建套接字//文件描述符为3
if ( sockfd == -1 )
{
return -1;
}
struct sockaddr_in saddr;//ipv4地址族,地址
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);//1024知名, 保留, 临时端口
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定ip,port,指定
if( res == -1 )
{
printf("bind err\n");
return -1;
}
res = listen(sockfd,5);//创建监听队列
if( res == -1 )
{
return -1;
}
return sockfd;
}
文件描述符是从3开始的,相当于下标,每当一个客户端与服务端连接,占用一个新的文件描述符,每当一个客户端关闭,这个文件描述符也就空闲出来,就可以接着被使用。