目录

1、TCP概览

1.1 TCP基本特征

1.2 TCP通信流程基本原理

2、TCP编程的函数接口说明

3、TCP通讯测试代码

1、TCP概览

TCP全称 Transmition Control Protocol,即:传输控制协议。是面向连接的协议。通常,TCP 通信还会被冠以可靠传输协议的头衔。但请注意,这里的可靠并非指发出去的数据对方一定能收到(这是不可能的),而仅指TCP能使发送方可靠地知道对方是否收到了数据。

1.1 TCP基本特征

  • 有连接:通信双方需要事先连接成功,方可传输数据
  • 有确认:一方收到对端的任何数据,都会给另一方发回执确认
  • 保证数据有序、不重复、丢失会重发
  • 如果网络拥堵,会自动调节发送量
  • 采用帧异步的流式通信方式(即通信双方每次的收发数据量不必相等)

简单来讲,TCP 类似于打电话,说话前需要花一定的时间接通电话,等到对方接听了之后双方才能开始通信,通信的过程中每个数据的传送,接收方都会给发送方回执确认,断开的时候也会互相通知以便于释放各自相关的资源。可以看出来,TCP 相对于 UDP 而言资源开销更大,提供更丰富的功能,TCP适合用在如下情形:

  • 传输质量要求较高,不能丢失数据
  • 大数据量的通信,以至于通信前后的连接和断开的开销可以忽略不计
  • 用户登录、账户管理等相关的功能

1.2 TCP通信流程基本原理

TCP的通信流程跟打电话是几乎一样的,因此可以将通信的过程细分为主动发起连接者(客户端)和被动接受连接者(服务端)两方来分别讨论。

被动的服务端

  1. 建立TCP套接字sockfd,即通信端点
  2. 绑定套接字sockfd与网络地址,即IP+端口
  3. 设定套接字sockfd进入被动监听状态,即将套接字设定为监听套接字
  4. 静静等待远程客户端的连接请求
  5. 收到连接请求后,得到一个专用于收发数据的连接套接字connfd
  6. 使用连接套接字connfd与客户端通信

主动的客户端

  1. 建立TCP套接字sockfd,即通信端点
  2. 对服务端发起连接请求
  3. 若连接成功,则直接通过套接字sockfd与服务端通信

注意点:

  • 在服务端中,监听套接字和连接套接字是严格区分的,不可混用
  • 服务端所绑定的地址(IP+PORT)需要对外公开,否则客户端无法发起连接
  • 客户端在发起连接前一般无需绑定地址,此时系统会为此连接自动分配恰当的地址资源

2、TCP编程的函数接口说明

TCP通讯的步骤如下:

客户端(client)

1、建立套接字(socket)

//2、绑定主机的IP地址和端口号(bind)(可以省略)

3、填充服务器的结构体,向服务器发起连接请求(connect)

4、聊天 — 发送数据(send/write)

5、断开连接(close)

服务器(server)

1、建立套接字(socket)

2、填充服务器的结构体,绑定主机的IP地址和端口号(bind)

3、设置监听(listen)

4、阻塞等待接收客户端的连接(accept)新的套接字文件描述符

5、创建结构体存放客户端IP和端口,聊天 — 接收数据 (recv/read)

6、断开连接(close)

函数接口说明如下:

1、建立套接字(socket)#include #include int socket(int domain,int type,int protocol);函数作用:建立套接字,返回套接字文件描述符函数参数:domain:你要选择哪一种地址族PF_INET/AF_INETIPV4网络协议PF ---> Protocol FamilyPF_INET6/AF_INET6 IPV6网络协议AF ---> Address Familytype:你要选择哪一种协议SOCK_STREAM选择TCP -- 流式套接字 --->Stream SocketsSOCK_DGRAM选择UDP -- 数据报套接字--->Datagram Socketsprotocol:传0表示使用默认协议返回值:成功:套接字文件描述符sockfd失败:-12、绑定主机的IP地址和端口号(bind)#include #include int bind(int sockfd,const struct sockaddr* addr,socklen_t addrlen);函数作用:绑定主机的IP地址和端口号参数:sockfd:套接字文件描述符 addr:自己的IP地址和端口号 addelen:地址的大小长度返回:成功3、发起连接(connect)#include #include int connect(int sockfd,const struct sockaddr * addr,socklen_t addrlen);函数作用:发起连接函数参数:sockfd:套接字文件描述符addr:对方的IP地址和端口号addrlen:地址的长度(sizeof(struct sockaddr_in)))IPV4结构体struct sockaddr_in{short int sin_family;unsigned short int sin_port;struct in_addr sin_addr;};struct in_addr{in_addr_t s_addr;/in_addr_t为32位的unsigned int,该无符号整数采用大端字节序/};初始化 IP地址和端口号 --IPV4struct sockaddr_in sereverAddr;serverAddr.sin_family = PF_INET;serverAddr.sin_port = htons(5000);serverAddr.sin_addr.s_addr = inet_addr("主机IP地址");4、聊天 -- 发送数据(send)#include #include ssize_t send(int sockfd,const void* buf,size_t len,int flags);函数作用:用于网络中发送数据参数:sockfd:套接字文件描述符buf:你要发送的数据len:你要发送数据的大小,以字节位单位flags:一般默认为0返回值:成功:发送的字节数失败:-15、断开连接(close)#include uint16_t htons(uint16_t hostshort);//将主机端口号转成网络端口号uint16_t ntohs(uint16_t netshort);//将网络端口号转成主机端口号说明:h代表主机(host) n代表网络(network) s代表端口号(short)返回值:成功:要转换的字节序失败:-1从什么(h,n)端口号到(to)什么(n,h)端口号(s)#include #include #include in_addr_t inet_addr(const char *cp);//将主机IP转成网络IPchar* inet_ntoa(struct in_addr in);//将网络IP转成主机IP主机转网络addr(address) 网络转主机ntoa(network to address)

tcp的服务端需要设置端口复用

端口复用设置在服务器的代码里面

#include

#include

int setsockopt(int s,int level,int optname,const void* optval,socklen_toptlen);

函数作用:设置端口号可以复用

参数:s:套接字文件描述符

level:代表欲设置的网络层,一般设成SOL_SOCKET以存取socket层

optname:SO_REUSEADDR端口号复用

optval:代表欲设置的值,比如给他一个1,表示使能 端口号复用

optlen则为optval的长度 //所以设置端口号可以复用,这两条语句放在绑定bind之前

int optval = 1;

setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval));

3、TCP通讯测试代码

//tcp_server.c#include #include #include #include #include #include #include #include #define SERVER_PORT 60000//-->1024 ~ 65535#define SERVER_IP "192.168.5.184"//-->虚拟机ip或主机ip/*TCP的服务端的代码实现步骤1、建立套接字2、绑定(IP和端口号)3、监听(listen)4、阻塞连接(accept)5、接收数据(recv,read)6、关闭*/int main(int argc,char** argv){//手动输入端口号和IP地址// if(argc != 3)// {// perror("./a.out ip port");// return -1;// }int ret = 0;char buf[1024] = { 0 };//1、建立套接字//参数:地址族 流式套接字 默认协议int socketfd = socket(AF_INET,SOCK_STREAM,0);if(socketfd == -1){perror("socket fail");return -1;}//设置端口复用int optval = 1;//参数套接字文件描述符网络层 端口复用设置值 设置值的大小setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR,&optval, sizeof(optval));//2、填充服务端结构体的端口和IPstruct sockaddr_in server_addr;server_addr.sin_family = AF_INET;//server_addr.sin_port = htons(atoi(argv[2]));//传参方式server_addr.sin_port = htons(SERVER_PORT);//宏定义方式//server_addr.sin_addr.s_addr = inet_addr(argv[1]); //传参方式server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); //宏定义方式//3、绑定//参数套接字文件描述符 IP和端口号(旧结构体指针强转取地址)新结构体的大小ret = bind(socketfd,(struct sockaddr*)&server_addr,sizeof(struct sockaddr_in));if(ret == -1){perror("bind fail");return -1;}//4、监听//参数 套接字文件描述符 支持的客户端连接最大数量ret = listen(socketfd,20);if(ret == -1){perror("bind fail");return -1;}//5、阻塞等待客户端连接//printf("绑定服务器IP:%s 端口号PORT:%hu\n",argv[1],atoi(argv[2]));printf("绑定服务器IP:%s 端口号PORT:%hu\n",SERVER_IP,SERVER_PORT);struct sockaddr_in client_addr;int len = sizeof(struct sockaddr_in);//参数 套接字文件描述符 IP和端口号(旧结构体指针强转取地址)长度取址//返回新的套接字文件描述符int newClientfd = accept(socketfd,(struct sockaddr*)&client_addr,&len);//6、拿到客户端的地址和端口号char* ip = inet_ntoa(client_addr.sin_addr);int port = ntohs(client_addr.sin_port);printf("客户端的IP:%s 端口号:%d\n",ip,port);//7、接收数据while(1){//缓存区清零bzero(buf,sizeof(buf));//接收数据,计算返回值(buf真实数据大小)//参数套接字文件描述符 缓存区 缓存区大小 默认属性//使用返回的新套接字文件描述符//ret = recv(newClientfd,buf,sizeof(buf),0);ret = read(newClientfd,buf,sizeof(buf));if(ret == 0){printf("客户端掉线,服务器退出。。。\n");return -1;}printf("收到数据:%s 大小:%d\n",buf,ret);//做主动退出的判断条件if(!strcmp(buf,"exit"))break;}//8、关闭套接字close(socketfd);close(newClientfd);return 0;}//tcp_client.c#include #include #include #include #include #include #include #include #define SERVER_PORT 60000//-->1024 ~ 65535#define SERVER_IP "192.168.5.184"//-->服务器的IP/*TCP的客户端的代码实现步骤1、建立套接字2、绑定(IP和端口号)(可有可无)3、发送连接请求(connect)4、发送数据(send,write)5、关闭*/int main(int argc,char** argv){//手动输入端口号和IP地址// if(argc != 3)// {// perror("./a.out ip port");// return -1;// }int ret = 0;char buf[1024] = { 0 };//1、建立套接字//参数:地址族 流式套接字 默认协议int socketfd = socket(AF_INET,SOCK_STREAM,0);if(socketfd == -1){perror("socket fail");return -1;}//2、填充服务端结构体的端口和IPstruct sockaddr_in server_addr;server_addr.sin_family = AF_INET;//server_addr.sin_port = htons(atoi(argv[2]));//传参方式server_addr.sin_port = htons(SERVER_PORT);//宏定义方式//server_addr.sin_addr.s_addr = inet_addr(argv[1]); //传参方式server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); //宏定义方式//3、连接服务器//参数 套接字文件描述符IP和端口号(旧结构体指针强转取地址)新结构体的大小ret = connect(socketfd,(struct sockaddr *)&server_addr,sizeof(struct sockaddr_in));if(ret < 0){perror("connect fail");return -1;}printf("连接服务器成功[%s][%d]\n",SERVER_IP,SERVER_PORT);//printf("连接服务器IP:%s 端口号PORT:%hu\n",argv[1],atoi(argv[2]));//4、与服务器之间发送数据while(1){//缓存区清零bzero(buf,sizeof(buf));//发送数据,计算返回值(buf真实数据大小)scanf("%s",buf);//参数套接字文件描述符 缓存区 缓存区真实大小 默认属性//ret = send(socketfd,buf,strlen(buf),0);ret = write(socketfd,buf,strlen(buf));if(ret== -1){printf("服务端掉线,客户端发送数据失败。。。\n");return -1;}printf("发送成功 ret:%d\n",ret);//做主动退出的判断条件if(!strcmp(buf,"exit"))break;}//关闭套接字close(socketfd);return 0;}