TCP傳輸
TCP的三次握手
圖解爲TCP協議的傳輸過程。
客戶端操作 | 服務端操作 |
---|---|
1、創建套接字 | 1、創建套接字 |
2、向服務端發起連接 | 2、綁定地址信息 |
3、發送數據 | 3、監聽(若有新的客戶端,新建socket) |
4、接收數據 | 4、接收已經連接成功的socket |
5、關閉套接字 |
TCP傳輸-socket編程
通過C++實現一個類來封裝tcp協議
//tcpsocket.hpp
#include <string>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.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() : _sockfd(-1){
}
void SetSockFd(int fd){
_sockfd = fd;
}
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 = 10) {
//int listen(int sockfd, int backlog);
//backlog:最大併發連接數--內核中已完成連接隊列的最大節點數
int ret = listen(_sockfd, backlog);
if (ret < 0) {
perror("listen error");
return false;
}
return true;
}
bool Connect(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 = connect(_sockfd, (struct sockaddr*)&addr, len);
if (ret < 0) {
perror("connect error");
return false;
}
return true;
}
bool Accept(TcpSocket &csock, struct sockaddr_in *addr = NULL){
struct sockaddr_in _addr;
socklen_t len = sizeof(struct sockaddr_in);
int newfd = accept(_sockfd, (struct sockaddr*)&_addr, &len);
if (newfd < 0) {
perror("accept error");
return false;
}
if (addr != NULL) {
memcpy(addr, &_addr, len);
}
csock.SetSockFd(newfd);
//_sockfd--僅用於接收新客戶端連接請求
//newfd----專門用於與客戶端進行通信
return true;
}
bool Recv(std::string &buf) {
char tmp[4096] = {0};
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) {
int ret = send(_sockfd, buf.c_str(), buf.size(), 0);
if (ret < 0) {
perror("send error");
return false;
}
return true;
}
bool Close() {
close(_sockfd);
_sockfd = -1;
}
private:
int _sockfd;
};
tcp傳輸客戶端
#include "tcpsocket.hpp"
int main(int argc, char *argv[])
{
if (argc != 3 ) {
std::cout<<"./tcp_cli ip port\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;
}
tcp傳輸服務端
#include "tcpsocket.hpp"
int main(int argc, char *argv[])
{
if (argc != 3) {
printf("./tcp_srv ip port\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;
struct sockaddr_in cliaddr;
//accept是阻塞獲取已經完成的連接
if (sock.Accept(clisock, &cliaddr) == false) {
continue;
}
printf("new connect client:%s:%d\n",
inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
std::string buf;
clisock.Recv(buf);
printf("client say:%s\n", buf.c_str());
buf.clear();
std::cout<<"server say:";
fflush(stdout);
std::cin>>buf;
clisock.Send(buf);
}
sock.Close();
return 0;
}
運行截圖如上
TCP的傳輸運行結果與UDP的傳輸運行結果不同
這是因爲在UDP的傳輸無連接,不可靠,面向數據報
但是TCP的傳輸不同是建立連接,可靠傳輸,面向字節流
所以基本的tcp服務端只能與一個客戶端通信一次,無法實現同時與多個客戶端多次通信
如何快速判斷連接是否已經斷開
tcp的連接管理中,內建有保活機制。當長時間沒有數據來往的時候,每隔一段時間都會向對方發送一個保活探測包,要求對方回覆,當多次發送保活探測包都沒有響應,則認爲連接斷開。
若連接斷開,則recv會返回0,send會觸發異常SIGPIPE(導致進程退出)。recv返回0不是代表沒有接收數據的意思,而是說明連接已經斷開。
在TCP連接過程中,因爲面向字節流,有可能接收到半條數據,因此一定要對返回值進行判斷,判斷數據是否已經完全接收或者完全發送。
多進程版本tcp服務端
通過使用多進程來完成多個客戶端的請求處理
將之前的通用版本改爲多線程版本即可
#include <signal.h>
#include <sys/wait.h>
#include "tcpsocket.hpp"
void sigcb(int no){
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc, char *argv[])
{
if (argc != 3) {
printf("./tcp_srv ip port\n");
return -1;
}
signal(SIGCHLD, sigcb);
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;
struct sockaddr_in cliaddr;
//accept是阻塞獲取已經完成的連接
if (sock.Accept(clisock, &cliaddr) == false) {
continue;
}
printf("new connect client:%s:%d\n",
inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
int pid = fork();
if (pid == 0) {
while(1){
std::string buf;
clisock.Recv(buf);
printf("client say:%s\n", buf.c_str());
buf.clear();
std::cout<<"server say:";
fflush(stdout);
std::cin>>buf;
clisock.Send(buf);
}
}
clisock.Close();
}
sock.Close();
return 0;
}
先進行多個客戶端連接,在左上角的服務端可以看到有三個新的客戶端進行了連接成功
這時候相繼發送消息,服務端的回覆順序是根據接收客戶端發送數據的順序進行回覆的
哪怕有其他客戶端搶先回答了,服務端還是按照發送數據的順序依次給客戶端進行發送數據
多線程版本tcp服務端
#include <pthread.h>
#include "tcpsocket.hpp"
void *thr_start(void *arg){
TcpSocket *sock = (TcpSocket*)arg;
while(1) {
std::string buf;
sock->Recv(buf);
printf("client say:%s\n", buf.c_str());
buf.clear();
std::cout<<"server say:";
fflush(stdout);
std::cin>>buf;
sock->Send(buf);
}
sock->Close();
delete sock;
return NULL;
}
int main(int argc, char *argv[])
{
if (argc != 3) {
printf("./tcp_srv ip port\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();
struct sockaddr_in cliaddr;
//accept是阻塞獲取已經完成的連接
if (sock.Accept(*clisock, &cliaddr) == false) {
continue;
}
printf("new connect client:%s:%d\n",
inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
pthread_t tid;
pthread_create(&tid, NULL, thr_start, (void*)clisock);
pthread_detach(tid);
//多線程中 ,主線程不能關閉socket,因爲線程之間共享文件描述符表
//如果在主線程中關閉了socket,其它線程中的這個描述符也就關閉了
}
sock.Close();
return 0;
}
線程版本的服務端與進程版本的服務端操作大同小異,差距就是進程與線程的創建方式不同,在方式上可能有區別。能瞭解這兩個知識就能掌握。