理解源
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 文件描述符 (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和port string peerip = inet_ntoa(peer.sin_addr); uint16_t peer_port=ntohs(peer.sin_port); //加入users CheckUser(peerip,peer_port,peer); //打印客户端发送来的数据 LogMessage(NOTICE,"[%s:%d]:%s",peerip.c_str(),peer_port,inbuffer); //消息路由:就是将客户端收到的所有数据发给每一个客户端 MessageRoutine(peerip,peer_port,inbuffer); } } private: int socketfd_;//服务器的socketfd uint16_t port_;//服务器的端口号 string ip_;//服务器的ip unordered_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对应的端口号和ip string 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
(获取连接的文件操作符),因为创建子进程前已经有listenfd
和servicefd
,子进程的数据结构是继承自父进程,所以建议子进程关闭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和read while (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和port uint16_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,会自动bind if(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协议的客户端/服务器程序流程
三次握手(建立连接)
服务器初始化:
- 调用
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
给服务器; (第四次)
- 可靠传输 vs 不可靠传输
- 有连接 vs 无连接
- 字节流 vs 数据报
第一次)- 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
- 客户端收到
SYN-ACK
后会从connect()返回, 同时应答一个ACK
段; (第三次)