> 文章列表 > 网络编程套接字

网络编程套接字

网络编程套接字

网络编程套接字

    • 网络编程基础知识
      • 理解源`IP`地址和目的`IP`地址
      • 理解源MAC地址和目的MAC地址
      • 认识端口号
      • 理解端口号和进程ID
      • 理解源端口号和目的端口号
      • 认识`TCP`协议
      • 认识`UDP`协议
      • 网络字节序
    • socket编程接口
    • `sockaddr`
    • `UDP`网络程序
      • 服务器端代码逻辑:
      • 需要用到的接口
      • 服务器端代码
      • `udp`客户端代码逻辑
      • `udp`客户端代码
    • `TCP`网络程序
      • 服务器代码逻辑
      • 多个版本服务器
        • 单进程版本
        • 多进程版本
        • 多线程版本
        • 线程池版本
      • 服务器端代码
      • 客户端代码逻辑
      • 客户端代码
    • TCP协议通讯流程
      • TCP协议的客户端/服务器程序流程
      • 三次握手(建立连接)
      • 数据传输
      • 四次挥手(断开连接)
    • TCP和UDP对比

网络编程基础知识

理解源IP地址和目的IP地址

  • IP是用来标识广域网中主机的唯一性
  • IP地址标识源主机
  • 目的IP地址标识目的主机

理解源MAC地址和目的MAC地址

MAC地址是在局域网中标识主机的唯一性,在数据包的报头中会有两个MAC地址,目的MAC地址和源MAC地址,MAC地址只在局域网中有效,在跨网络的传输中,A主机MAC地址->路由器MAC地址,路由器MAC地址->B主机MAC地址

所以,数据包的报头数据中目的ip和源ip是不变的,但是报头数据中的源MAC和目的MAC地址是会变化的

认识端口号

两台主机进行网络通信知道ip地址就可以了吗?

  • 主机间的通信本质上是A主机的进程和B主机的进程进行通信所以我们需要使用到端口号(是传输层协议的内容)

  • 端口号是用来标识一台主机上进程的唯一性

  • 端口号是一个2字节16位的整数(uint16_t)

  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程

  • 一个端口号只能被一个进程占用

  • 一个进程可以绑定多个端口号

理解端口号和进程ID

端口号和进程ID都是用来标识进程的唯一性,有什么区别?

  • pid是操作系统管理进程的角度
  • 端口号是进程通信的角度

理解源端口号和目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”

认识TCP协议

  • 传输层协议
  • 面向连接
  • 可靠传输
  • 面向字节流

认识UDP协议

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

网络字节序

在内存中的数据的字节序有大端(低位数据放到高地址处)小端(低位数据放到低地址处)之分,那么如果两台主机进行网络通信,字节序不同,如何解决这种问题?

我们规定网络数据流的数据为大端字节序,我们在发送数据前,需要先将数据由主机字节序转换成网络字节序,接受数据后,将网络字节序转换成主机字节序

四个接口网络主机序列转换

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint32_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint 16_t netshort);

socket编程接口

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器) 
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

sockaddr

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,然而, 各种网络协议的地址格式并不相同

通过socket接口既可以支持网络通信,又可以支持本地通信,将struct sockaddr_in,struct sockaddr_un强转sockaddr,在API内部通过sockaddr的前16位表示是网络通信(sockaddr_in)还是本地通信(sockaddr_un)

在这里插入图片描述

UDP网络程序

进行网络通信,就需要客户端程序和服务器端程序

服务器端代码逻辑:

1、创建客户端套接字
2、填充网络信息
3、绑定网络信息
4、接受客户端发来的数据
5、向发送来数据的客户端,发送反馈数据

需要用到的接口

创建套接字

int socket(int domain, int type, int protocol);
/创建套接字/参数:
/domain(通信类型):本地通信还是网络通信
/type(数据类型):数据类型是数据报还是数据流
/protocol(协议协议):网络应用中设为0/返回值:如果套接字创建失败返回-1,创建成功返回一个文件描述符,其实就是打开了网卡文件

绑定网络信息

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
/将addr绑定到sockfd文件描述符对应的套接字/sockfd:需要绑定套接字对应的文件描述符
/addr:结构体指针,结构体内填充网络信息(协议家族,ip,port)
/addrlen:addr指向结构体的大小/返回值:成功返回0,失败返回-1

接收数据

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
/接收数据/参数:
/sockfd:接收方套接字对应的文件描述符
/buf:接收的数据放到buf中
/len:接收数据的长度
/flags:0
/src_addr(输出型参数):发送方struct sockaddr_in地址,因为需要向发来数据的发送方发送数据
/addrlen(输入输出型参数):发送方struct sockaddr_in的大小/返回值:成功返回接收数据的字节数,失败返回-1

发送数据

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
/发送数据/参数:
/sockfd:发送方套接字对应的文件描述符
/buf:发送的数据放到buf中
/len:发送数据的长度
/flags:0
/dest_addr(输出型参数):接受方struct sockaddr_in地址,因为需要向接收方发送数据
/addrlen(输入输出型参数):接受方struct sockaddr_in的大小/返回值:成功返回发送数据的字节数,失败返回-1

点分十进制转四字节

in_addr_t inet_addr(const char *cp);
/cp:ip地址点分十进制
/返回值:ip地址四字节形式

四字节转点分十机制

char *inet_ntoa(struct in_addr in);
/in:ip地址四字节形式
/返回值:ip地址点分十进制
  • 注意:点分十进制转四字节,四字节转点分十机制时,会自动转换字节序

  • 注意:发送数据时,struct socketaddr_in,也会发送过去,所以填充网络信息时需要将主机字节序转换成网络字节序

  • **接受数据时,将网络信息提取出来,需要将网络字节序转换成主机字节序 **

服务器端代码

class udpserver
{
public:udpserver(int port,string ip=""):port_(port),ip_(ip),socketfd_(-1){}~udpserver(){}void init(){//1、创建socket套接字//本地通信还是跨网络通信,数据类型(数据流还是数据报),协议类型:网络应用中设为0//返回值是文件描述符socketfd_ = socket(AF_INET,SOCK_DGRAM,0);if (socketfd_ < 0){LogMessage(FATAL,"socket:%s,%d",strerror(errno),socketfd_);exit(1);}else{LogMessage(DEBUG,"socket create success:%d",socketfd_);}//2、绑定网络信息//2.1填充网络信息struct sockaddr_in local;bzero(&local,sizeof(local));//讲local的每一个字节都置为0//填充协议家族,域(就是前16个字节)local.sin_family=AF_INET;//填充端口号local.sin_port=htons(port_);//字节序转换:主机转网络//填充ip//INADDR_ANY地址为0,填充任意一个ip地址//IP地址的作用:可以在网络当中唯一标识一台主机,一个公网IP地址只能被一台机器所占有,一个机器可以拥有多个IP地址。//填充ip地址需要填充四字节的格式,inet_addr是将一个指定的ip由点分十进制转换到四字节,同时也会自动转换字节序//一台服务器可能有一个ip地址,也可能有多个ip地址,INADDR_ANY,会绑定这台主机的所有ip地址local.sin_addr.s_addr=ip_.empty()?htons(INADDR_ANY):inet_addr(ip_.c_str());//2.2绑定网络信息if(bind(socketfd_,(const sockaddr*)&local,sizeof(local))==-1){LogMessage(FATAL,"bind:%s:%d",strerror(errno),socketfd_);exit(2);}else{LogMessage(DEBUG,"bind sucess:%d",socketfd_);}}
//检查用户void CheckUser(string userip,uint16_t userport,struct sockaddr_in &peer){string key=userip;key+=":";key+=to_string(userport);if(users.find(key)==users.end()){users.insert({key,peer});}}
//将服务器收到的数据发给每一个客户端void MessageRoutine(string ip,uint16_t port,string info){string message="[";message+=ip;message+=":";message+=to_string(port);message+="]:";message+=info;//将收到的数据发送给所有客户端for(auto& i : users){sendto(socketfd_,message.c_str(),message.size(),0,(const sockaddr*)&i.second,sizeof(i.second));}}void start(){char inbuffer[1024];    //将来读取到的数据放在这里char outbuffer[1024];   //这是要发送的数据while(true){//LogMessage(NOTICE," server提供服务中");//将读取到的数据放到inbuffer//peer是通过网络发过来的客户端信息(发送方的网络信息),是网络字节序,后续使用需要转换成主机字节序struct sockaddr_in peer;      // 一个结构体,存放的是客户端的信息,因为要回信息,需要知道是谁发的,输出型参数socklen_t len = sizeof(peer); // peer的长度,输入输出型参数// 接受数据size_t s = recvfrom(socketfd_,inbuffer,sizeof(inbuffer)-1,0,(struct sockaddr*)&peer,&len);if(s>0){inbuffer[s]=0;}else if(s==-1){LogMessage(FATAL,"recvfrom:%s:%d",strerror(errno),socketfd_);continue;}//接收成功,获取客户端的ip和portstring peerip = inet_ntoa(peer.sin_addr);uint16_t peer_port=ntohs(peer.sin_port);//加入usersCheckUser(peerip,peer_port,peer);//打印客户端发送来的数据LogMessage(NOTICE,"[%s:%d]:%s",peerip.c_str(),peer_port,inbuffer);//消息路由:就是将客户端收到的所有数据发给每一个客户端MessageRoutine(peerip,peer_port,inbuffer);}}private:int socketfd_;//服务器的socketfduint16_t port_;//服务器的端口号string ip_;//服务器的ipunordered_map<string,struct sockaddr_in> users;//online users
};int main(int argc, char *argv[])
{cout<<"pid:"<<getpid()<<endl;if(argc!=2&&argc!=3){cout<<"failed"<<endl;exit(3);}uint16_t port=atoi(argv[1]);string ip;if(argc==3){ip=argv[2];}udpserver svr(port,ip);svr.init();svr.start();return 0;
}

udp客户端代码逻辑

1、创建客户端套接字
2、填充网络信息
3、客户端不需要主动绑定,os会自动绑定
4、客户端发送数据
5、接受服务器端发送来的数据
  • 为什么客户端不建议主动绑定?

    因为会有多个客户端来连接服务器端,一个端口只能被一个进程占有,如果一个客户端主动绑定指定的端口,那么该端口可能已经被占用了,就会发生错误

  • 为什么服务器端不能os自动绑定?

    因为服务器端是要提供服务的,客户端是要来连接服务器端,所以端口号不能随意改变,而客户端不会被来连接,所以可以由os自动绑定(端口号随意分配)

udp客户端代码

void *recverAndPrint(void *args)
{while (true){int sockfd = *(int *)args;char buffer[1024];struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << "server echo# " << buffer << std::endl;}}
}// ./udpclient ip port
int main(int argc,char* argv[])
{cout<<"pid:"<<getpid()<<endl;if(argc!=3){exit(1);}//1、提取客户端要来连接的服务器的ip//client必须知道要连接的server对应的端口号和ipstring server_ip=argv[1];uint16_t server_port=atoi(argv[2]);//2、创建客户端int socketfd = socket(AF_INET,SOCK_DGRAM,0);if (socketfd < 0){LogMessage(FATAL, "socket:%s,%d", strerror(errno), socketfd);exit(1);}else{LogMessage(DEBUG, "socket create success:%d", socketfd);}//客户端不需要我们主动bind,os会帮我们绑定,os会随机分配port,第一次使用sendto函数会自动bind//3、发送数据struct sockaddr_in server;string buffer;bzero(&server,sizeof(server));server.sin_family=AF_INET;//主机转网络字节序,发送数据时端口号和ip也会发送过去server.sin_port=htons(server_port);//点分十进制转四字节server.sin_addr.s_addr=inet_addr(server_ip.c_str());pthread_t t;pthread_create(&t, nullptr, recverAndPrint, (void *)&socketfd);while (true){cerr << "Please Enter: " << endl;getline(cin, buffer);//向服务器端发送数据sendto(socketfd, buffer.c_str(), buffer.size(), 0,(const struct sockaddr*)&server,sizeof(server));}return 0;
}

在这里插入图片描述

  • 127.0.0.1:本地环回(就是数据通过网络协议栈到达最底层,不往网络里发,直接向上交付到主机的另一个进程)

TCP网络程序

服务器代码逻辑

  • 创建套接字
  • 填充网络信息
  • 将套接字和网络协议地址绑定
  • 设置监听状态(监听状态,服务器就可以获取来连接了)(因为TCP是面向连接,所以进行工作前需要先建立连接)
  • 获取连接
  • 提供服务

设置监听状态

int listen(int socket, int backlog);
/socket:套接字
/backlog:5
//返回值:成功返回0,失败返回-1

获取连接

int accept(int socket, struct sockaddr *restrict address,socklen_t *restrict address_len);
/socket:套接字
/address:指向网络协议地址
/len:网络协议地址长度
/返回值:返回一个文件描述符,这个文件描述符是提供服务的,服务对这个文件描述符对应的文件进行读写

多个版本服务器

单进程版本

  • 多个客户端同时连接服务器端,服务器只会为一个客户端提供服务?

    因为单进程版本是一个但执行流,所以在完成一个客户端的服务后才会,为下一个客户端服务

  • 但是,多个客户端却可以同时建立连接成功?

    当服务端在给第一个客户端提供服务期间,第二个客户端向服务端发起的连接请求是成功的,但是此时服务器端没有调用accept函数获取该连接.

    实际在底层会为我们维护一个连接队列,服务端没有accept的新连接就会放到这个连接队列当中,而这个连接队列的最大长度就是通过listen函数的第二个参数来指定的,因此服务端虽然没有获取第二个客户端发来的连接请求,但是在第二个客户端那里显示是连接成功的。

多进程版本

  • 父进程获取连接,子进程提供服务

  • 子进程提供服务需要close(获取连接的文件操作符),因为创建子进程前已经有listenfdservicefd,子进程的数据结构是继承自父进程,所以建议子进程关闭listenfd

  • 为什么不能阻塞?

    不能使用waitpid,因为父进程调用waitpid时,子进程可能还没有退出,父进程就会阻塞住了,只有子进程提供服务结束,父进程才会获取连接,这样还是串行

  • 如何不阻塞?

    • 如果服务端采用非阻塞的方式等待子进程,虽然在子进程为客户端提供服务期间服务端可以继续获取新连接,但此时服务端就需要将所有子进程的PID保存下来,并且需要不断花费时间检测子进程是否退出
    • 捕捉SIGCHLD信号,将其处理动作设置为忽略(子进程退出,os会向父进程发送SIGCHLD信号)
    • 让父进程创建子进程,子进程再创建孙子进程,最后让孙子进程为客户端提供服务(爷爷进程获取连接,孙子进程提供服务,等待父进程,孙子进程被1号进程进程领养,自动等待)

多线程版本

  • 主线程获取链接,其余线程提供服务

线程池版本

  • 先创建多个线程,主线程获取获取连接,客户端发送数据,主线程将任务放入线程池中,线程池中多个线程排队执行任务,线程池中的多个线程是执行任务的,不是客户端数量,客户端发送连接,服务器获取链接,生产任务
  • 线程池需要把任务push进线程池中去,线程池中的线程执行任务

服务器端代码

class tcp;
class threadData
{
public:string clientip_;uint16_t clientport_;int sockfd_;tcp* ts_;
public:threadData(string clientip,uint16_t clientport,int sockfd,tcp* ts):clientip_(clientip),clientport_(clientport),sockfd_(sockfd),ts_(ts){}};void service(int servicefd,string clientip,uint16_t clientport)
{// 2.1、获取客户发送的数据// 数据流使用write和readwhile (true){char buffer[1024];ssize_t s = read(servicefd, buffer, sizeof(buffer)-1);if (s > 0){buffer[s]=0;if(strcasecmp(buffer,"quit")==0){LogMessage(DEBUG,"service[%s][%d]:quit1",clientip.c_str(),clientport);break;}LogMessage(NOTICE,"service[%s][%d]:transbefor:%s",clientip.c_str(),clientport,buffer);for(int i=0;i<s;i++){if(isalpha(buffer[i])&&islower(buffer[i])){buffer[i]=toupper(buffer[i]);}}LogMessage(NOTICE,"service[%s][%d]:transafter:%s",clientip.c_str(),clientport,buffer);write(servicefd,buffer,strlen(buffer));//全双工通信}else if(s==0){//写端关闭LogMessage(DEBUG,"service[%s][%d]:quit2",clientip.c_str(),clientport);break;}else{LogMessage(FATAL, "read:%s:%d", strerror(errno), servicefd);break;}}close(servicefd);LogMessage(DEBUG,"service finish[%s][%d]",clientip.c_str(),clientport);
}//获得命令的结果
void execCommand(int servicefd,string clientip,uint16_t clientport)
{char command[1024];while (true){ssize_t s = read(servicefd, command, sizeof(command) - 1);if (s > 0){command[s] = 0;FILE* out=popen(command,"r");if(out==nullptr){LogMessage(WARNING,"execCommand[%s][%d]:popen failed",clientip.c_str(),clientport);break;}char work[1024]={0};while (fgets(work, sizeof(work) - 1, out) != nullptr){write(servicefd, work, strlen(work));}pclose(out);LogMessage(DEBUG, "[%s:%d] exec [%s] ... done", clientip.c_str(), clientport, command);// 全双工通信}else if (s == 0){// 写端关闭LogMessage(DEBUG, "execCommand[%s][%d]:quit2", clientip.c_str(), clientport);break;}else{LogMessage(FATAL, "read:%s:%d", strerror(errno), servicefd);break;}}close(servicefd);LogMessage(DEBUG, "execCommand finish[%s][%d]", clientip.c_str(), clientport);
}class tcp
{
public:tcp(uint16_t port,string ip="",int sockfd=-1):ip_(ip),port_(port),threadpool_(nullptr){}~tcp(){}//4、多线程版本// static void* Routine(void* argv)// {//     threadData* client=(threadData*)argv;//     pthread_detach(pthread_self());//     service(client->sockfd_,client->clientip_,client->clientport_);//     delete(client);//     return nullptr;// }void init(){//1 创建服务器端listensockfd_=socket(AF_INET,SOCK_STREAM,0);if(listensockfd_==-1){LogMessage(FATAL,"socket:%s:%d",strerror(errno),listensockfd_);exit(1);}else{LogMessage(NOTICE,"socket success:%d",listensockfd_);}//2 绑定网络信息//2.1 填充网络信息struct sockaddr_in local;local.sin_family=AF_INET;local.sin_port=htons(port_);local.sin_addr.s_addr=ip_.empty()?htons(INADDR_ANY):inet_addr(ip_.c_str());//2.2 bindsockaddr_in和套接字绑定if(bind(listensockfd_,(const sockaddr*)&local,(socklen_t)sizeof(local))==-1){LogMessage(FATAL,"bind:%s:%d",strerror(errno),listensockfd_);exit(2);}LogMessage(DEBUG,"bind success:%d",listensockfd_);//3 将socket设置监听状态,因为tcp是面向连接的(面向连接:需要先建立连接,再进行工作)if(listen(listensockfd_,5)==-1){LogMessage(FATAL,"listen:%s:%d",strerror(errno),listensockfd_);exit(3);}//设置监听状态后,允许别人来连接你LogMessage(DEBUG,"listen success:%d",listensockfd_);//加载线程池threadpool_=tcp::threadpool_->getinstance();}void loop(){//5、线程池先创建多个线程threadpool_->start();LogMessage(DEBUG, "thread pool start success, thread num: %d", threadpool_->thread_num());//signal(SIGCHLD, SIG_IGN);while(true){// 1、获取连接,accept的返回值是一个新的sockfd// 传入的参数sockfd,这个套接字是用来获取新的连接(客户端来连接)// 返回值sockfd,这个套接字主要是为客户提供网络服务struct sockaddr_in peer;socklen_t len=sizeof(peer);int servicefd_ = accept(listensockfd_,(struct sockaddr*)&peer,&len);if(servicefd_==-1){LogMessage(WARNING,"accept:%s:%d",strerror(errno),listensockfd_);exit(1);}LogMessage(NOTICE,"accept success:%d",listensockfd_);//2、进行服务//提取clientip和portuint16_t clientport=ntohs(peer.sin_port);string clientip=inet_ntoa(peer.sin_addr);//1、单进程版//service(servicefd_,clientip,clientport);//2、多进程版//父进程获取连接,子进程提供服务//不能使用waitpid,因为父进程调用waitpid时,子进程可能还没有退出,父进程就阻塞住了// int pid = fork();// if(pid==0)// {//     close(listensockfd_);//     service(servicefd_,clientip,clientport);//     exit(0);// }// close(servicefd_);//3、多进程版本//爷爷进程获取连接,孙子进程提供服务,等待父进程,孙子进程被1号进程领养,自动等待// int pid = fork();// if(pid==0)//爸爸进程// {//     int id = fork();//     if(id==0)    //孙子进程//     {//         close(listensockfd_);//         service(servicefd_,clientip,clientport);//         exit(0);//     }//     else//     {//         exit(0);//     }// }// close(servicefd_);//文件描述符泄露// //孙子进程被bash收养,退出后,由系统自动回收,不会影响其他进程// //爷爷进程调用waitpid时,爸爸进程肯定已经退出了,爷爷进程不会阻塞等待// waitpid(pid,nullptr,0);//4、多线程版本//主线程获取链接,其余线程提供服务// threadData* client=new threadData(clientip,clientport,servicefd_,this);// pthread_t tid;// pthread_create(&tid,nullptr,Routine,(void*)client);//5、线程池版本//先创建多个线程,主线程获取获取连接,客户端发送数据,主线程将任务放入线程池中,线程池中多个线程排队执行任务//线程池中的多个线程是执行任务的,不是客户端数量//客户端发送连接,服务器获取链接,生产任务//线程池需要把任务push进线程池中去,线程池中的线程执行任务// Task ts(servicefd_,clientip,clientport,service);// threadpool_->push(ts);//5、线程池版本,命令行执行结果服务Task ts(servicefd_,clientip,clientport,execCommand);threadpool_->push(ts);}}
private:int listensockfd_;uint16_t port_;string ip_;threadpool<Task> *threadpool_;};int main(int argc,char* argv[])
{cout<<"hello"<<endl;if(argc!=2&&argc!=3){cout<<"failed"<<endl;exit(3);}string ip;if(argc==3){ip=argv[2];}uint16_t port=atoi(argv[1]);tcp svr(port,ip);svr.init();svr.loop();return 0;
}

将命令执行的结果以文件的方式读到

FILE *popen(const char *command, const char *type);//将命令执行的结果以文件的方式读到
/command:命令
/type:打开文件的方式/popen底层原理:
/1、创建管道
/2、创建子进程
/3、子进程执行命令,将数据放到管道,父进程以文件读到

客户端代码逻辑

  • 创建套接字
  • 发送连接
  • 发送数据

发送连接

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
/sockfd:客户端套接字
/addr:服务器端的网络协议地址
/addrlen:addr的长度
/返回值:成功返回0,失败返回-1

客户端代码

bool quit=false;
int main(int argc,char*argv[])
{assert(argc==3);//首先提取客户端需要连接服务器端的ip和端口string serveip=argv[1];uint16_t serveport=atoi(argv[2]);//1、创建客户端int sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd==-1){LogMessage(FATAL,"socket:%s:%d",strerror(errno),sockfd);}LogMessage(DEBUG,"socket success:%d",sockfd);//2、发送连接//不用监听,因为客户端不会获取连接,没有人来连接客户端struct sockaddr_in serve;serve.sin_family=AF_INET;serve.sin_port=htons(serveport);serve.sin_addr.s_addr=inet_addr(serveip.c_str());socklen_t len=sizeof(serve);//connect发送连接,调用connect,会自动bindif(connect(sockfd,(struct sockaddr *)&serve,len)==-1){LogMessage(FATAL,"connect:%s:%d",strerror(errno),sockfd);exit(1);}LogMessage(DEBUG,"conect success:%d",sockfd);//3、向服务器端发送数据char buffer[1024];string message;while (!quit){message.clear();cout << "请输入你的消息>>> ";getline(cin,message);if (strcasecmp(message.c_str(), "quit") == 0)quit = true;ssize_t s = write(sockfd, message.c_str(),message.size());if (s > 0){message.resize(1024);ssize_t s = read(sockfd, (char*)message.c_str(), 1024);if (s > 0)message[s] = 0;std::cout << "Server Echo>>> " << message << std::endl;}else if (s <= 0){break;}}close(sockfd);return 0;
}

TCP协议通讯流程

TCP协议的客户端/服务器程序流程

在这里插入图片描述

三次握手(建立连接)

在这里插入图片描述

服务器初始化:

  • 调用socket, 创建文件描述符;
  • 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
  • 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
  • 调用accecpt, 并阻塞, 等待客户端连接过来;

建立连接的过程:

  • 调用socket, 创建文件描述符;
  • 调用connect, 向服务器发起连接请求;
  • connect会发出SYN段并阻塞等待服务器应答; (第一次)
  • 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
  • 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)

这个建立连接的过程, 通常称为三次握手**

数据传输

在这里插入图片描述

  • 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
  • 服务器从accept()返回后立刻调用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
  • 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
  • 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
  • 客户端收到后从read()返回, 发送下一条请求,如此循环下去

四次挥手(断开连接)

在这里插入图片描述

  • 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
  • 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
  • read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
  • 客户端收到FIN, 再返回一个ACK给服务器; (第四次)

TCP和UDP对比

  • 可靠传输 vs 不可靠传输
  • 有连接 vs 无连接
  • 字节流 vs 数据报
    第一次)
  • 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
  • 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)