TCP客户端编程开发
任何的网络编程套接字开发的两种工作模式:TCP网络、UDP网络。
TCP和UDP的介绍
TCP:连接式网络通信,长连接通信或流式通信。TCP的通信一般稳定、可靠,但传输速度往往没有UDP快。其中有这样一个概念----心跳时间,用以保持长链接。而它可靠的原因就是他的三次握手(链接)和四次挥手(断开连接)。类似于打电话。
UDP:报式网络通信,短链接通信方法,或者说无链接。相较于TCP来说,安全可靠性低,但速度超高。类似于发短信。
两者如何选择,取决于在编程中输入的参数。
TCP的三次握手和四次挥手
- 三次握手
第一次握手:客户端向服务器发送一个 SYN包,其中包含客户端随机生成的初始序列号,记为 seq=x。这个包的作用是向服务器表明客户端想要建立连接,并告知服务器自己的初始序列号。此时客户端进入 SYN_SENT 状态。
第二次握手:服务器接收到客户端的 SYN 包后,会向客户端发送一个 SYN+ACK 包。该包中,确认号为客户端的序列号加 1,即 ack=x+1,表示服务器已经收到了客户端的 SYN 包,并且准备好接收客户端的数据。同时,服务器也会随机生成一个自己的初始序列号 seq=y。此时服务器进入 SYN_RCVD 状态。
第三次握手:客户端收到服务器的 SYN+ACK 包后,会向服务器发送一个 ACK 包。该包的确认号为服务器的序列号加 1,即 ack=y+1,序列号为客户端在第一次握手中发送的序列号加 1,即 seq=x+1。服务器收到这个 ACK 包后,连接建立成功,双方进入 ESTABLISHED 状态,开始进行数据传输。
- 四次挥手
第一次挥手:主动关闭方(通常是客户端)发送一个 FIN(Finish)包,其中包含主动关闭方的序列号 seq=u,表示主动关闭方已经没有数据要发送了,请求关闭连接。此时主动关闭方进入 FIN_WAIT_1 状态。
第二次挥手:被动关闭方收到 FIN 包后,会发送一个 ACK 包,确认号为主动关闭方的序列号加 1,即 ack=u+1,序列号为被动关闭方自己的序列号 seq=v。此时被动关闭方进入 CLOSE_WAIT 状态,而主动关闭方收到 ACK 包后进入 FIN_WAIT_2 状态。
第三次挥手:被动关闭方在完成数据处理后,也会发送一个 FIN 包,其中序列号为 seq=w(如果在收到 FIN 包后没有新的数据发送,w=v+1),确认号仍然为 ack=u+1。此时被动关闭方进入 LAST_ACK 状态。
第四次挥手:主动关闭方收到被动关闭方的 FIN 包后,会发送一个 ACK 包进行确认,确认号为 ack=w+1,序列号为 u+1。主动关闭方发送完这个 ACK 包后进入 TIME_WAIT 状态,等待一段时间(通常为 2 倍的 MSL,最长报文段寿命)后,如果没有收到被动关闭方的重传请求,则认为连接已经成功关闭,进入 CLOSED 状态。被动关闭方收到 ACK 包后,也会进入 CLOSED 状态。
TCP 的三次握手和四次挥手机制,可以理解为不断加包的过程,保证了网络中数据传输的可靠性和稳定性,使得客户端和服务器之间能够准确地建立连接和关闭连接。
Linux下TCP客户端开发接口函数:
socket();//创建套接字
connect(); //链接xxx服务器
write();//通过套接字发送数据给服务器
read(); //通过套接字读取服务器发来的消息
close(); //关闭链接的服务器
socket();
函数功能:创建一个套接字
函数头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型: int socket(int domain, int type, int protocol);
函数参数:
domain:AF_INET : IPV4 | AF_INET6: IPV6
type: 你创建的套接字的类型-> TCP/UDP
SOCK_STREAM->流式套接字 TCP
SOCK_DGRAM->报式套接字 UDP
protocol: 如果不是原始套接字 这个固定填 0
函数返回值:成功返回一个文件描述符,失败返回负数。基本上不可能失败,除非当前的进程打开的文件超出上限!
Call back open函数,这就好比用open函数打开一个普通文件,open返回的是这个文件的描述符,而socket 返回的是套接字对应的描述符。你之后对这个返回的描述符进行操作,就相当于在操作这个套接字,比如发送和接收数据。
connect();
函数功能:利用套接字链接其他的服务器(可以是内网服务器/也可以公网服务器
函数头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型: int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数参数:
- sockfd:链接服务器的套接字
- addr:如果使用的是IPV4的地址,建议使用struct sockaddr_in 结构体充当第二个参数。第二个参数主要作用是提供连接服务器:链接类型(IPV4/IPV6) 、端口号、IP地址。
PS:所有网络编程的存储方式为大端!所以在填入端口号时,需要利用htons将原本的小端格式转换为大端格式。例如,htons(55555)==985,小端格式存储的55555,转化为大端格式就是985。
而在填入地址时,也要注意地址本质是一个uint32_t类型的数字。所以需要快速直接转换整型 IP 变成大端 可以借助iner_addr函数。例如,整型的大端 IP = inet_addr(字符串 IP)。
函数返回值: 该函数会阻塞,直到出现以下两种情况:
链接服务器通过!返回0 | 链接失败/超时,返回负数
TCP客户端获取高德天气数据
#include "sys/types.h"
#include "stdlib.h"
#include "unistd.h"
#include "string.h"
#include "sys/socket.h"
#include "stdio.h"
#include <pthread.h>
#include <arpa/inet.h>
#include "netinet/in.h"
#define POST "GET https://restapi.amap.com/v3/weather/weatherInfo?city=410102&key=15dfb2a0ae03b142a72afbc9cbbd47e4\r\n"
pthread_t pd;
int skd;
int main(){
//1. 创建套接字
skd = socket(AF_INET, SOCK_STREAM,0);
if(skd < 0){
perror("socket");
exit(0);
}
printf("套接字创建成功 skd==%d\r\n", skd);
//2.创建sock_addr
struct sockaddr_in server_addr = {0};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(80);//一般是8080或80,使用时可以试一下是哪个
server_addr.sin_addr.s_addr = inet_addr("106.11.226.133");//高德开发的IP地址
printf("sockaddr: %d\n",server_addr.sin_addr.s_addr);
//3. 连接服务器
int ret = connect(skd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if ((ret<0))
{
perror("connect");
exit(0);
}
printf("连接服务器成功\r\n");
char send_buf[2048] = {0};
printf("POST==%s\r\n",send_buf);
usleep(100*100);
write(skd,POST,103);
usleep(100*100);
read(skd,send_buf,sizeof(send_buf));
printf("send_buf==%s\n",send_buf);
return 0;
}
实验现象:
TCP服务器编程开发
服务器
服务器就是有被别人连接能力的电脑。服务器分内网和公网两种:
内网服务器:若服务器程序使用的是电脑从路由器或交换机获取的 IP 地址,那么该程序仅能被处于同一网络的主机访问。例如,在 Ubuntu 系统中,服务器所在电脑的 IP 地址为 192.168.222.128 ,只有连接到同一路由器、网关或交换机的主机才能连接此服务器。使用手机热点上网的设备则无法连接该服务器。
公网服务器:要搭建公网服务器,需向联通、移动、电信等运营商申请开通静态 IP 上网服务,与运营商协商使宽带 IP 固定,并获取一定的对外访问权限。理论上,这样搭建的服务器可被全国范围内的用户通过网络连接。但实际上,运营商通常不会支持此类操作。较为可行的做法是将服务器程序部署到阿里云、百度云、腾讯云等云服务器上,如此一来,全球任何位置的用户都能访问该服务器程序。
服务器程序的开发流程:
- 需要创建一个TCP的套接字----socket()
- 绑定服务器的属性(IP地址、端口号)----bind()
- 监听->监测/允许链接的最大数----listen()
- 接收客户端的链接----accept()
- 与客户端通信----read()/write()
重点是,就服务器而言,必须要有固定的IP地址和端口号!
在多网卡的情况下,绑定不同的IP地址相当于使用不同网卡上网。
Linux下TCP服务器开发接口函数:
bind()
函数原型:int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
函数功能:绑定给服务器(套接字)固定的IP地址和端口号
函数头文件:
- #include <sys/types.h>
- #include <sys/socket.h>
函数参数:
- sockfd:绑定给服务器的套接字
- addr:绑定给服务器的IPV4/IPV6、IP地址、端口号等属性。建议使用struct sockaddr_in类型
- addrlen:上个参数的长度
函数返回值:成功返回0 | 失败返回负数
listen()
函数原型:int listen(int sockfd, int backlog);
函数功能:监听服务器,设置服务器的最大连接数
函数头文件:
- #include <sys/types.h>
- #include <sys/socket.h>
函数参数:
- sockfd:设置的套接字(服务器)
- backlog:允许链接/要监听的数量
函数返回值:成功返回0 | 失败返回负数
accept()
函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
函数功能:接收一个客户的链接。如果调用该函数的时候没有客户链接,则会阻塞,等到有客户链接的时候,便立即返回,达到同步的效果。
函数参数:
- sockfd:服务器的套接字
- addr:用来存放客户返回的属性
- addr_len:一般创建一个socklen_t len;len=sizeof(struct sockaddr_in);并把&len传入其中
函数返回值:返回值是客户的套接字。后续的与该用户通信需要用到这个套接字。
TCP服务器通信测试
#include "sys/types.h"
#include "stdlib.h"
#include "unistd.h"
#include "string.h"
#include "sys/socket.h"
#include "stdio.h"
#include <pthread.h>
#include <arpa/inet.h>
#include "netinet/in.h"
void * Client_read_data_output(void * arg);
int ckd[64]={0};
int ckdcount = 0;
int main(){
//1. 创建套接字
int skd = socket(AF_INET, SOCK_STREAM, 0);
if(skd < 0){
perror("socket");
exit(0);
}
printf("套接字创建成功,skd==%d\r\n", skd);
//2. 定义sockaddr_in 结构体,并填充所需参数
struct sockaddr_in server_addr = {0};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(11112);
server_addr.sin_addr.s_addr = inet_addr("172.20.10.2");
//3. 给当前套接字绑定一个IP地址和端口号
int tmp = bind(skd, (struct sockaddr *)&server_addr,sizeof(server_addr));
if(tmp<0){
perror("bind");
exit(0);
}
//4. 监听
tmp = listen(skd,64);//最大允许64个连接
pthread_t pd;
//5.轮询地接受客户连接
struct sockaddr_in Clien_addr;
socklen_t len = sizeof(Clien_addr);
while(1){
ckd[ckdcount++] = accept(skd,(struct sockaddr *)&Clien_addr,&len);
printf("现在有%d个连接到了我的服务器\r\n",ckdcount);
pthread_create(&pd, NULL, Client_read_data_output, &ckd[ckdcount-1]);
}
return 0;
}
void * Client_read_data_output(void * arg)
{
int ckd = *((int *)arg);//套接字传入线程
char buff[1024]={0};
int len = 0;
while(1){
memset(buff, 0, sizeof(buff));
len = read(ckd, buff, sizeof(buff));
if(len==0){
printf("客户%d离线了", ckd);
close(ckd);
pthread_exit(NULL);
}
printf("来自客户%d的消息:%s\n",ckd,buff);
}
}
实验现象: