TCP:Transmission Control Protocol,传输控制协议,传输层的一个非常重要的协议,负责数据传送。面向连接、传输可靠、但通信效率较低。
基于 TCP 通常采用 C/S 模式,即客户端(Client)/服务器(Server)模式,客户端程序和服务器端程序的实现不一样,它们是不对等的。每次通信都是由客户端主动发起(即连接服务器),服务器端被动参与通信(即接收客户端的连接请求)。
网络传输的数据包由两部分组成:一是协议所要用到的首部,另一部分是上层传过来的数据。TCP 的首部承载着 TCP 协议协议的各项信息:
TCP 首部预留了两个16位给端口号存储,也就是端口号的范围是2^16 = 65535,前1024以下给系统保留,其他的作为用户使用的端口范围;
32位序号 seq:(Sequence number),TCP 通信过程中某一个传输方向上的字节流的每个字节的序号,通过这个序号来确认发送数据的顺序。
32位确认号 ack:(Acknowledge number),TCP 对上一次的seq序号做出的确认号,用来响应 TCP 报文段,给收到的 TCP 报文段的序号seq+1;
在 socket 编程中,客户端执行 connect()完成;
**第一次握手:**客户端将 TCP 报文标志位SYN置为1,随机产生一个序号值seq=J,保存在TCP首部的序列号字段里,指明客户端打算连接的服务器的端口,并将数据包发送给服务器端,发送完毕后,客户端进入(SYN_SENT)等待状态;
**第二次握手:**服务器端收到数据包后由标志位SYN = 1知道客户端请求建立连接,服务器端将TCP报文标志位SYN和ACK都置为1,ack=J+1,随机产生一个序号seq = K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态;
第三次握手:客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack = K+1,并将该数据包发送给服务器端,服务器端检查是否为K+1,ACK是否为1如果正确则连接建立成功,客户端和服务器端进入 ESTABLISHED 状态,完成三次握手,随后客户端与服务器端之间就可以开始传输数据了;
通过调用close()函数完成
FIN_WAIT_1
状态,这表示Client端没有数据要发送给Server端了。FIN_WAIT_2
状态,Server端告诉Client端,我确认并同意你的关闭请求。LAST_ACK
状态。TIME_WAIT
状态。Server端收到Client端的ACK报文段以后,就关闭连接。此时,Client端等待2MSL的时间后依然没有收到回复,则证明Server端已正常关闭,那好,Client端也可以关闭连接了。基于 TCP 协议通信的过程如同打电话的通信过程。
socket
函数实现,不用花钱!bind
函数,不用花钱!listen
函数实现。accept
函数实现。send
/write
和 recv
/read
函数实现。close
函数实现。close
函数实现。套接字(Socket):网络接入点,即进程必须通过套接字接入计算机网络中才能进行通信。
socket
函数实现,不用花钱!bind
函数实现,可以选择一个很酷的端口号,不用花钱!(显式绑定)在第三步调用
connect
函数时,connect
函数内部会检测套接字是否已经成功绑定地址,如果没有显式绑定或绑定失败了,会随机选择一个空闲端口号和本机任意 IP 跟套接字绑定起来(隐式绑定)
connect
函数实现。send
/write
,recv
/read
函数实现。close
函数实现简易实例:服务器端{发一句,收一句}
#include
#include
#include
#include
#include
#include
#include
int main()
{
//socket 函数:创建一个新套接字
//参数意义:
//第一个参数:地址家族,通常是 AF_INET 族
//第二个参数:套接字类型,通常有:SOCK_STREAM(流套接字,用于 TCP 协议通信)和 SOCK_DGRAM
//第三个参数:通常为 0,表示使用默认协议
//返回值为新套接字的文件描述符,如果失败则为 -1
//第一步:创建一个新的监听套接字
int sock_listen = socket(AF_INET, SOCK_STREAM, 0);
if(-1 ==sock_listen)
{
perror("socket");
return 1;
}
//第二步:绑定地址
//指定地址信息
struct sockaddr_in myaddr;
myaddr.sin_family = AF_INET; //指定地址家族为 Internet 地址家族
myaddr.sin_addr.s_addr = INADDR_ANY; //指定 IP 地址为本机任意地址
//myaddr.sin_addr.s_addr = inet_addr("192.162.0.32"); //指定 IP 地址为本机某个确定 IP
myaddr.sin_port = htons(8888); //指定端口号为 8888 (D)——22B8(H)小端——B822(H)大端——47138(D)
//printf("%d\n",htons(8888));
//htons:将一个短整型数据从主机字节序(通常是小端)转换为网络字节序(通常是大端)
//inet_addr:将字符串形式的 IP 地址转换为无符号 32 位整数,并且是网络字节序。
//将上面指定的地址和套接字绑定
if(bind(sock_listen, (struct sockaddr*)&myaddr, sizeof(myaddr)))
{
perror("bind");
return 1;
}
//第三步:监听
//listen 函数的第二个参数为监听等待队列的长度
if(-1 == listen(sock_listen, 5))
{
perror("listen");
return 1;
}
//第四步:接受客户端的请求
//accept(sock_listen, NULL, NULL);//如果对客户端地址信息不感兴趣
//如果想获取当前客户端的地址信息,就使用下面的方式:
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
//调用 accept 函数接收一个客户端连接请求(队头请求)
//如果成功,返回值为一个套接字文件描述符,这个套接字是和该客户端一一对应的
//这个套接字专门用于和对应的客户端通信,所以通常称它为连接套接字。
//accpte
int sock_conn = accept(sock_listen, (struct sockaddr*)&client_addr, &len); //第二个为结构体的指针,第三个参数为结构体的长度
if(-1 ==sock_conn)
{
perror("accept");
}
//第五步:发送数据
char msg[100] = "我是服务器!";
double d = 3.14;
//send(sock_conn, msg, sizeof(msg), 0);//最后一个参数为发送标准,默认为 0
//send(sock_conn, &d, sizeof(d), 0);
int ret = write(sock_conn, msg, sizeof(msg));
//write(sock_conn, &d, sizeof(d));
if(ret == sizeof(msg))
{
printf("发送成功!\n");
}
//接收数据,如果当时没有任何数据,recv 函数会阻塞当前线程,直到成功收到数据或对方断开连接或出错。
ret = recv(sock_conn, msg, sizeof(msg), 0); //同理用 read 等效
//recv(sock_conn, &d, sizeof(d), 0);
if(ret > 0)
{
printf("客户端说:%s\n",msg);
}
//第六步:断开客户端连接
close(sock_conn);
//第七步:关闭监听套接字
close(sock_listen);
return 0;
}
简易实例:客户端{收一句,发一句}
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char** argv)
{
//socket 函数:创建一个新套接字
// 参数意义:
// 第一个参数:地址家族,通常是 AF_INET
// 第二个参数:套接字类型,通常有两种:SOCK_STREAM(流套接字,用于 TCP 协议通信) 和 SOCK_DGRAM(数据报式套接字,用于 UDP 协议通信)
// 第三个参数:通常为 0, 表示使用默认协议。
// 返回值为新套接字的文件描述符,如果失败则为 -1
// 第一步:创建一个新的套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sock)
{
perror("socket");
return 1;
}
// // 第二步:绑定地址
// // 指定地址信息
// struct sockaddr_in myaddr;
// myaddr.sin_family = AF_INET; // 指定地址家族为 Internet 地址家族
// myaddr.sin_addr.s_addr = INADDR_ANY; // 指定 IP 地址为本机任意地址
// //myaddr.sin_addr.s_addr = inet_addr("192.168.0.56"); // 指定 IP 地址为本机某个确定的 IP 地址
// myaddr.sin_port = htons(8888); // 指定端口号为 8888
// // htons:将一个短整型数据从主机字节序(通常是小端)转换为网络字节序(通常是大端)
// // inet_addr:将字符串形式的 IP 地址转换为无符号 32 位整数,并且是网络字节序
// // 将上面指定的地址和套接字绑定
// if(-1 == bind(sock, (struct sockaddr*)&myaddr, sizeof(myaddr)))
// {
// perror("bind");
// return 1;
// }
// 第三步:连接服务器
// 指定服务器的地址
struct sockaddr_in srv_addr;
srv_addr.sin_family = AF_INET;
srv_addr.sin_addr.s_addr = inet_addr(argv[1]);
srv_addr.sin_port = htons(atoi(argv[2]));
if(-1 == connect(sock, (struct sockaddr*)&srv_addr, sizeof(srv_addr)))
{
perror("connect");
return 1;
}
// 第四步:收发数据
// 发送数据
char msg[100];
int ret;
// // 接收数据,如果当前没有任何数据,recv 函数会阻塞当前线程,直到成功接收到数据或对方端开连接(返回 0)或出错(返回 -1)。
ret = recv(sock, msg, sizeof(msg), 0);
// // read(sock_conn, msg, sizeof(msg)); // 和上面的写法等效
if(ret > 0)
{
printf("服务器端说:%s\n", msg);
}
strcpy(msg, "你是傻子!");
ret = send(sock, msg, sizeof(msg), 0);
//write(sock_conn, msg, sizeof(msg)); // 和上面的写法等效
if(ret == sizeof(msg))
{
printf("发送成功!\n");
}
// 第五步:端开连接
close(sock);
return 0;
}
//服务器端:循环等待发送数据 ./执行文件 文件(不能是文件夹)
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char** argv)
{
//socket 函数:创建一个新套接字
int sock_listen = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sock_listen)
{
perror("socket");
return 1;
}
// 第二步:绑定地址
// 指定地址信息
struct sockaddr_in myaddr;
myaddr.sin_family = AF_INET;
myaddr.sin_addr.s_addr = INADDR_ANY;
myaddr.sin_port = htons(8888);
// 将上面指定的地址和套接字绑定
if(-1 == bind(sock_listen, (struct sockaddr*)&myaddr, sizeof(myaddr)))
{
perror("bind");
return 1;
}
// 第三步:监听
if(-1 == listen(sock_listen, 5))
{
perror("listen");
return 1;
}
while(1)
{
// 第四步:接受客户端连接请求
//accept(sock_listen, NULL, NULL); // 如果对客户端地址信息不感兴趣
// 如果想获取当前客户端的地址信息,就使用下面的方式
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int sock_conn = accept(sock_listen, (struct sockaddr*)&client_addr, &len);
if(-1 == sock_conn)
{
perror("accept");
continue;
}
printf("\n%s:%hu 已连接...\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 第五步:收发数据
// 发送数据
char file_name[300];
char msg[1024];
int ret;
unsigned long cnt1 = 0, cnt2 = 0;
time_t start, end;
char* p = NULL;
p = strrchr(argv[1], '/');
if(NULL == p)
{
strcpy(file_name, argv[1]);
}
else
{
strcpy(file_name, p + 1);
}
send(sock_conn, file_name, sizeof(file_name), 0);
int fd = open(argv[1], O_RDONLY);
printf("正在努力发送文件...\n");
time(&start);
while((ret = read(fd, msg, sizeof(msg))) > 0)
{
cnt1 += ret;
ret = send(sock_conn, msg, ret, 0);
//write(sock_conn, msg, sizeof(msg)); // 和上面的写法等效
if(ret == -1)
{
printf("发送文件失败!\n");
break;
}
cnt2 += ret;
}
if(cnt1 != 0 && cnt1 == cnt2)
{
time(&end);
printf("发送文件成功!(耗时 %d 秒,平均网速 %d B/s)\n", end - start, cnt1 / (end -start));
}
// 第六步:断开客户端连接
close(sock_conn);
}
// 第七步:关闭监听套接字
close(sock_listen);
return 0;
}
// 客户端 输入 ./执行文件 IP Port
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char** argv)
{
// 第一步:创建一个新的套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sock)
{
perror("socket");
return 1;
}
// 第三步:连接服务器
struct sockaddr_in srv_addr;
srv_addr.sin_family = AF_INET;
srv_addr.sin_addr.s_addr = inet_addr(argv[1]);
srv_addr.sin_port = htons(atoi(argv[2]));
if(-1 == connect(sock, (struct sockaddr*)&srv_addr, sizeof(srv_addr)))
{
perror("connect");
return 1;
}
// 第四步:收发数据
// 发送数据
char msg[300];
int ret;
ret= recv(sock,msg,sizeof(msg), 0);
int fd = open("msg",O_WRONLY | O_CREAT);
while(1)
{
ret = recv(sock, msg, sizeof(msg), 0);
if(ret <= 0) break; //0为正常断开,-1为异常断开
write(fd,msg,ret);
}
close(fd);
// 第五步:端开连接
close(sock);
return 0;
}