套接字编程---2(TCP套接字编程的流程,TCP套接字编程中的接口函数,TCP套接字的实现,TCP套接字出现的问题,TCP套接字多进程版本,TCP套接字多线程版本)
TCP模型创建流程图
TCP套接字编程中的接口
socket 函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:
- AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
- AF_INET6 与上面类似,不过是来用IPv6的地址
- AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
type:
- SOCK_STREAM 这个协议是按照顺序的、可靠的、 数据完整的基于字节流的连接。 这是一个使用最多的socket类型,这个socket 是使用TCP来进行传输。
- SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
- SOCK_SEQPACKET该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
- SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。 (ping、traceroute使用该协议)
- SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序
protocol:
传0 表示使用默认协议。
返回值
成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno
socket函数的作用
socket()打开一个网络通讯端口,如果成功的话,就像 open()一样返回一个文件描述符,应用程序可以像读写文件一样用 read/write 在网络上收发数据,如果 socket()调用出错则返回-1。对于 IPv4,domain 参数指定为 AF_INET。 对于 TCP 协议,type 参数指定为 SOCK_STREAM,表示面向流的传输协议。如果是 UDP 协议,则 type 参数指定为 SOCK_DGRAM,表示面向数据报的传输协议。protocol 参数的介绍从略,指定为 0 即可。
bind 函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:
socket文件描述符
addr:
构造出IP地址加端口号
addrlen:
sizeof(addr)长度 返回值: 成功返回0,失败返回-1, 设置errno
bind函数的作用
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可 以向服务器发起连接,因此服务器需要调用 bind 绑定一个固定的网络地址和端口号。
**bind()的作用是将参数 sockfd 和 addr 绑定在一起,使 sockfd 这个用于网络通讯的文件描述符监听 addr 所描述 的地址和端口号。**前面讲过,
structsockaddr*
是一个通用指针类型,addr 参数实际上可以接受多种协议的 sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen 指定结构体的长度。如:struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(6666);首先将整个结构体清零,然后设置地址类型为
AF_INET
,网络地址为INADDR_ANY
,这个宏表示本地的任意 IP 地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个 IP 地址,这样设置可以在所有的 IP 地址上监听,直 到与某个客户端建立了连接时才确定下来到底用哪个 IP 地址,端口号为 6666。
accept 函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf:
socket文件描述符
addr:
传出参数,返回链接客户端地址信息,含IP地址和端口号
addrlen:
传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
返回值:
成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno
accpet函数的作用
三方握手完成后,服务器调用 accept()接受连接,如果服务器调用 accept()时还没有客户端的连接请求,就阻塞 等待直到有客户端连接上来。addr 是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen 参数是一 个传入传出参数(value-resultargument),传入的是调用者提供的缓冲区 addr 的长度以避免缓冲区溢出问题,传出 的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区) 。如果给 addr 参数传 NULL,表示不关心 客户端的地址。
示例
while (1) { cliaddr_len = sizeof(cliaddr); connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); n = read(connfd, buf, MAXLINE); ......close(connfd);}
整个是一个 while 死循环,每次循环处理一个客户端连接。由于 cliaddr_len 是传入传出参数,每次调用 accept() 之前应该重新赋初值。accept()的参数 listenfd是先前的监听文件描述符,而 accept()的返回值是另外一个文件描述符 connfd,之后与客户端之间就通过这个 connfd 通讯,最后关闭 connfd断开连接,而不关闭 listenfd,再次回到循环 开头 listenfd 仍然用作 accept 的参数。accept()成功返回一个文件描述符,出错返回-1。
connect 函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockdf:
socket文件描述符
addr:
传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen:
传入参数,传入sizeof(addr)大小
返回值:
成功返回0,失败返回-1,设置errno
客户端需要调用 connect()连接服务器,connect 和 bind 的参数形式一致,区别在于 bind 的参数是自己的地址, 而 connect 的参数是对方的地址。connect()成功返回 0,出错返回-1。
C/S 模型-TCP
Tcp通信的实现
封装接口
tcpsocket.hpp
/* * 封装Tcpsocket类,向外提供更加轻便的tcp套接字接口* 1.创建套接字 Socket()* 2.绑定地址信息 Bind(std::string &ip,uint16_t port)* 3.服务端开始监听 Listen(int backlog = 5)* 4.服务端获取已完成连接的客户端socket Accept(TcpSocket &clisock,std::string &cli_ip,uint16_t port)* 5.接受数据 Rec(std::string &buf)* 6.发送数据 Send(std::string &buf)* 7.关闭套接字 Close()* 8.客户端向服务器发起连接请求 Connect(std::string &srv_ip,uint16_t srv_port)*/#include<iostream>
#include<string>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>#define CHECK_RET(q) if((q) == false){return -1;} class TcpSocket
{public:TcpSocket(){} ~TcpSocket(){} bool Socket(){_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if(_sockfd < 0){ perror("socket error");return false;} return true;} bool Bind(std::string &ip,uint16_t port){struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = inet_addr(ip.c_str());socklen_t len = sizeof(struct sockaddr_in);int ret = bind(_sockfd, (struct sockaddr *)&addr ,len);if(ret < 0){perror("bind error");return false;}return true;}bool Listen(int backlog = 5){//int listen (int sockfd,int backlog)//sockfd: 套接字描述符//backlog:最大并发连接数--决定内核中已完成连接队列结点个数//backlog决定的不是服务端能接受的客户端最大上限int ret =listen(_sockfd,backlog);if(ret < 0){perror("listen error");return false;}return true;}bool Accept(TcpSocket &cli,std::string &cliip,uint16_t &port){//int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);//sockfd:套接字描述符//addr: 新建连接的客户端地址信息//addrlen: 新建客户端的地址信息长度//返回值:返回新建客户端socket的描述符struct sockaddr_in addr;socklen_t len = sizeof(struct sockaddr_in);int sockfd = accept(_sockfd,(sockaddr*)&addr,&len);if(sockfd < 0){perror("accept error");return false;}cli.SetFd(sockfd);cliip = inet_ntoa(addr.sin_addr);port = ntohs(addr.sin_port); return true; }void SetFd(int sockfd){_sockfd = sockfd;}bool Connect(std::string &srv_ip,uint16_t srv_port){//int connect(int sockfd,const struct sockaddr *adrr,socklen_t addrlen);//sockfd :套接字描述符//addr:服务端地址信息//addrlen:地址信息长度struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(srv_port);addr.sin_addr.s_addr = inet_addr(srv_ip.c_str());socklen_t len = sizeof(struct sockaddr_in);int ret = connect(_sockfd,(struct sockaddr*)&addr,len);if(ret < 0){perror("connect error");return false;}return true;}bool Recv(std::string &buf){//ssize_t recv(int sockfd,void *buf ,size_t len,int flags)//sockfd :服务端为新客户端新建的socket描述符//flags: 0--默认阻塞接受 没有数据一直等待// MSG_PEEK 接受数据,但是数据并不从缓冲区移除// 常用于探测性数据接受//返回值:实际接收字节长度;出错返回-1;若连接断开则返回0//recv默认阻塞的,意味着没有数据则一直等,不会返回0//返回0只有一种情况,就是连接断开,不能再继续通信了char tmp[4096];int ret = recv(_sockfd,tmp,4096,0);if(ret < 0){perror("recv error");return false;}else if(ret == 0){printf("peer shutdown\n");return false;}buf.assign(tmp,ret);return true;}bool Send(std::string &buf){//ssize_t send (int sockfd,void *buf,size_t len,int flags)int ret = send(_sockfd,buf.c_str(),buf.size(),0);if(ret < 0){perror("send error");return false;}return true;}bool Close(){close(_sockfd);return true;}private:int _sockfd;
};
客户端实现
/**基于封装的Tcpsocket实现tco客户端程序1.创建套接字2.为套接字绑定地址信息(不推荐用户手动绑定)while(1){
4.发送数据
5.接收数据}6.关闭套接字*/#include"tcpsocket.hpp"int main(int argc,char *argv[]){if(argc != 3){ std::cout<<"./tco_cli 192.168.145.132 9000\n";return -1; } std::string ip = argv[1];uint16_t port = atoi(argv[2]);TcpSocket sock;CHECK_RET(sock.Socket());CHECK_RET(sock.Connect(ip,port));while(1){std::string buf;std::cout<<"client say:";fflush(stdout);std::cin >> buf;sock.Send(buf);buf.clear();sock.Recv(buf);std::cout<<"server say:"<<buf<<std::endl; } sock.Close();return 0;
}
服务端的实现
/**基于封装的tcpsocket,实现tcp服务端程序1. 创建套接字2.为套接字绑定地址信息3.开始监听,如果有连接进来,自动完成三次握手while(1){4,从已完成连接队列,获取新建的客户端连接socket5.通过新建的客户端连接socket,与指定的客户端进行通信,recv6.send}7. 关闭套接字*/#include"tcpsocket.hpp"int main(int argc,char *argv[]){if(argc != 3){ std::cout<<"./tcp_srv 192.168.145.132 9000\n";return -1; } std::string ip =argv[1];uint16_t port =atoi(argv[2]);TcpSocket sock;CHECK_RET(sock.Socket());CHECK_RET(sock.Bind(ip,port));CHECK_RET(sock.Listen());while(1){TcpSocket clisock;std::string cliip;uint16_t cliport; if(sock.Accept(clisock,cliip,cliport) == false){ //当已完成的连接队列中没有socket,会阻塞 continue;} std::cout<<"new client:"<<cliip<<":"<<cliport<<std::endl;std::string buf;clisock.Recv(buf);std::cout<<"client say:"<<buf<<std::endl;buf.clear();std::cout<<"server say:";fflush(stdout);std::cin>>buf;clisock.Send(buf);}sock.Close();
}
程序出现的问题
这个实现的最基本的tcp服务端程序中,因为服务端不知道客户端数据什么时候到来,因此程序只能写死;但是写死就有可能会造成阻塞(accep/recv),导致服务端无法同时处理多个客户端的请求
多进程tcp服务端程/多线程版本tcp服务端程序
适用多进程tcp服务端程序的处理多客户端请求;每当一个客户端的连接到来,都创建一个新的子进程,让子进程单独与客户端进行通信;这样的话父进程永远只处理新连接
TCP套接字多进程版本
#include<signal.h>
#include"tcpsocket.hpp"
#include<sys/wait.h>void sigcb(int no){while(waitpid(-1,NULL,WNOHANG) > 0);
}int main(int argc,char *argv[]){if(argc != 3){std::cout<<"./tcp_srv 192.168.145.132 9000\n";return -1;}std::string ip =argv[1];uint16_t port =atoi(argv[2]);signal(SIGCHLD,sigcb);TcpSocket sock;CHECK_RET(sock.Socket());CHECK_RET(sock.Bind(ip,port));CHECK_RET(sock.Listen());while(1){TcpSocket clisock;std::string cliip;uint16_t cliport; if(sock.Accept(clisock,cliip,cliport) == false){ //当已完成的连接队列中没有socket,会阻塞continue;}std::cout<<"new client:"<<cliip<<":"<<cliport<<std::endl;int pid =fork();if(pid == 0){//子进程专门处理每个客户端的通信while(1){std::string buf;clisock.Recv(buf);std::cout<<"client say:"<<buf<<std::endl;buf.clear();std::cout<<"server say:";fflush(stdout);std::cin>>buf;clisock.Send(buf);}clisock.Close();exit(0);}//父进程关闭套接字,因为父子进程数据独有//不关闭,会造成资源泄露clisock.Close();//父进程不通信}sock.Close();
}
TCP套接字多线程版本
#include"tcpsocket.hpp"
#include<pthread.h>
void* thr_start(void *arg){TcpSocket *clisock = (TcpSocket *)arg;while(1){//因为线程之间,共享文件描述符表,因此在一个线程中打开的文件//另一个线程只要能够获取文件描述符,就能在操作文件std::string buf;clisock->Recv(buf);std::cout<<"client say:"<<buf<<std::endl;buf.clear();std::cout<<"server say:";fflush(stdout);std::cin>>buf;clisock->Send(buf);}clisock->Close();delete clisock;return NULL;
}int main(int argc,char *argv[]){if(argc != 3){std::cout<<"./tcp_srv 192.168.145.132 9000\n";return -1;}std::string ip =argv[1];uint16_t port =atoi(argv[2]);TcpSocket sock;CHECK_RET(sock.Socket());CHECK_RET(sock.Bind(ip,port));CHECK_RET(sock.Listen());while(1){TcpSocket *clisock = new TcpSocket();std::string cliip;uint16_t cliport; if(sock.Accept(*clisock,cliip,cliport) == false){ //当已完成的连接队列中没有socket,会阻塞continue;}std::cout<<"new client:"<<cliip<<":"<<cliport<<std::endl;pthread_t tid;pthread_create(&tid,NULL,thr_start,(void *)clisock);pthread_detach(tid);}sock.Close();
}
tcp连接断开
tcp自己实现了保活机制:当长时间没有数据通信,服务端会向客户端发送保活探测包;当这些保活探测包连续多次都没有响应,则认为连接断开
recv返回0;send触发异常
套接字编程---2(TCP套接字编程的流程,TCP套接字编程中的接口函数,TCP套接字的实现,TCP套接字出现的问题,TCP套接字多进程版本,TCP套接字多线程版本)相关推荐
- Linux网络编程 | Socket编程(二)TCPSocket的封装、TCP服务器多进程、多线程版本的实现
目录 TCP的通信流程 TCPSocket的封装 TCP客户端 TCP服务器 多进程版本 多线程版本 TCP的通信流程 计算机网络 (三) 传输层 :一文搞懂UDP与TCP协议 在这篇博客中,我描述了 ...
- Linux下套接字详解(五)----基于fork多进程的TCP套接字(阻塞/同步/并发)
简介 一个简单的改进方案是在服务器端使用多线程(或多进程).多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接.具体使用多进程还是多线程,并没有 ...
- 【Linux网络编程学习】使用socket实现简单服务器——多进程多线程版本
此为牛客Linux C++课程和黑马Linux系统编程笔记. 1. 多进程版 1.1 思路 大体思路与上一篇的单进程版服务器–客户端类似,都是遵循下图: 多进程版本有以下几点需要注意: 由于TCP是点 ...
- C采用多线程和多进程实现TCP/UDP网络连接
文章目录 基础版 多进程版本 多线程版本 UDP实现 基础版 封装的socket.h warp_socket.h #ifndef _WRAP__ #define _WRAP__#include < ...
- 基于TCP/UDP的socket服务器搭建流程
目录 TCP/UDP概念介绍 一.TCP/UDP对比 二.端口号的作用 三.字节序 1.概念 2.分类 3.C程序在内存空间的映射 socket服务器与客户端开发流程 一.流程介绍 二.socket服 ...
- Python并行编程(七):多进程的基本使用和与多线程的差异
进程 由于GIL的存在,Python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在Python中大部分情况需要使用多进程.Python提供了非常好用的多进程模块multip ...
- TCP报文格式和三次握手——三次握手三个tcp包(header+data),此外,TCP 报文段中的数据部分是可选的,在一个连接建立和一个连接终止时,双方交换的报文段仅有 TCP 首部。...
from:https://blog.csdn.net/mary19920410/article/details/58030147 TCP报文是TCP层传输的数据单元,也叫报文段. 1.端口号:用来标识 ...
- (chap2 TCP/IP基础知识) TCP/IP分层模型的通信流程
1. 每个分层中,都会对所发送的数据附件一个首部,在这个首部中包含了该层次必要的信息,如发送的目标地址以及协议相关信息. 1.1 数据单位 包 全能性术语. 帧 表示数据链路层中包的单位. 数据报 I ...
- 一万一千字!结合代码超详细讲解SQL执行流程(二)!干货到底!建议收藏!
上文我们已经学习到查询SQL语句的执行过程中如何获取 BoundSql!接下来继续从查询SQL语句的执行过程中如何创建 StatementHandler!喜欢的朋友们可以来个一键三连哦~ 目录 查询S ...
最新文章
- 控件包含代码块,因此无法修改控件集合
- 反思脑机接口技术:机器真的能控制我们的大脑吗?
- Collection、泛型
- nginx监听事件流程
- Opencl cl_khr_fp16
- 图片旋转 rotate
- RabbitMq学习笔记005---登录rabbitmq报错User can only log in via localhost
- 身陷 Bug 时,优秀的开发工程师是如何寻求帮助的?
- String.GetEnumerator 方法的C#例子
- leetCode:reverseInteger 反向整数 【JAVA实现】
- FFmpeg —— MP4文件提取h264文件
- 爬取千库网ppt_Python——如何实现千图成像:初级篇(从图片爬取到图片合成)...
- 八种常规常用的SQL查询语句
- “十四五”融合地表水国控断面监测数据(约3641个点位,含水质等级、水温,pH,溶解氧,电导率,浊度,高猛酸盐指数,化学需氧量,五日生化需氧量,氨氮,总磷,总氮,铜,锌,氟化物,硒,砷,汞,镉,等)
- tiny4412 uboot 2020.10版本移植(三)——uboot初步启动
- 从今往后要认真记录自己的成长啦
- SpringBoot上传大文件并支持中途取消上传
- Android游戏开发---碰撞检测
- 迎合国家新政策,共享购联合共享经济,三方互利,消费增值
- CSS中的块级元素、行内元素和行内块元素
热门文章
- TMS320F28335——IO控制/定时计操作
- Building a RESTful Web Service
- SIT与UAT的分别
- java sundry tips
- -bash: belts.awk: command not found
- Linux下动态库使用小结
- linux ptrace 内核源码分析,linux 3.5.4 ptrace源码分析分析(系列一)
- 2017计算机三级哪个好考,快速突破2017年计算机三级考试的几大复习阶段
- unity烘培单个物体_Unity可编程渲染管线(SRP)教程:二、自定义着色器
- mysql 的数据库实例理解_理解数据库和实例