一。【window为例】TCP协议的解释(记忆方法)
物理结构(自己理解)
1.服务器端
服务器端:
首先使用套接字函数创建 套接字 (Socket),并使用 绑定 函数绑定到本地地址(bind)。 使用 listen 指定传入连接的积压工作(listen),然后使用 accept 函数接受连接(accept)。最后接收数据(recv)。
1.创建套接字socket().(确定协议)
int socket( int af, int type, int protocol);af:协议族。支持AF_INET格式(ipv4)和AF_INET6格式(ipv6)
type:指定socket类型。如TCP(SOCK_STREAM)和UDP(SOCK_DGRAM)
rotocol:就是指定协议,如调用者不想指定,可用0。常用的协议有,IPPROTO_TCP、IPPROTO_UDP。
返回值:若无错误发生,socket()返回引用套接字的描述字。否则的话,返回INVALID_SOCKET错误,应用程序可通过WSAGetLastError()获取相应错误代码。
2. 绑定bind() :(确定具体ip与端口)
服务器必须在一个已知的名称(ip地址、端口)监听,属于显性绑定。客户端不需要调用bind绑定端口,而是由内核自定选择临时端口,属于隐性绑定。int PASCAL FAR bind( SOCKET sockaddr, const struct sockaddr FAR* my_addr,int addrlen);
sockaddr表示已经建立的socket编号(描述符);
FAR是一个指向sockaddr结构体类型的指针;
addrlen表示my_addr结构的长度,可以用sizeof操作符获得。
ip地址使用的是网络字节顺序,所以需要调用htons、htonl 函数主机把ip地址和端口的主机字节序转换网络字节序。对服务器而言 对于IPv4来说,通配地址通常由INADDR_ANY来指定,其值一般为0。它告知内核去选择IP地址。
如无错误发生,则bind()返回0。否则的话,将返回-1。
如果tcp服务器没有把ip地址捆绑到套接字上,内核就把客户端发送的syn的目的ip地址作为服务器的源ip地址。服务器可以通过getsockname函数获取该地址。
在bind函数调用的sockaddr 类型指针,可以直接用前面的sockaddr_in变量转换替换。
iPv4通配地址由INADDR_ANY指定,如果端口号为0表示由内核选择一个临时端口。
套接字分为UDP套接字和TCP套接字,对于前者,其由(ip地址,端口号)来标识,后者由(源ip,源端口号,目的ip,目的端口号)标识。
tcp:如多个不同的客户端连接服务端,会产生多个套接字,这些套接字实际上是共用了相同的服务端端口号,但源ip地址不一样。
疑问:如果是多个套接字,或者是多个进程应用绑定在同一ip,端口上,会怎样?tcp、udp都可以吗?
默认的情况下,如果一个套接字 绑定了一个端口,这时候,别的套接字就无法使用这个端口。但是端口复用允许在一个应用程序可以把 n 个套接字绑在一个端口上而不出错。
3.监听 listen(激活套接字)
int listen( int sockfd, int backlog);sockfd:用于标识一个已捆绑未连接套接口的描述字。
backlog:等待连接队列的最大长度。
如无错误发生,listen()返回0。否则的话,返回-1,应用程序可通过WSAGetLastError()获取相应错误代码。
该函数应该在socket和bind函数之后,accept函数之前。套接字会从closed状态转换到listen状态。
4.接受连接 accept (三次握手成功)
SOCKET accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);默认是阻塞函数
sockfd:套接字描述符,该套接口在listen()后监听连接。
addr:(可选)指针,指向一缓冲区,其中接收为通讯层所知的连接实体的地址(即客户端的地址)。Addr参数的实际格式由套接口创建时所产生的地址族确定。
addrlen:(可选)指针,输入参数,配合addr一起使用,指向存有addr地址长度的整型数。
返回值:如果没有错误产生,则accept()返回一个描述所接受包的SOCKET类型的值。否则的话,返回INVALID_SOCKET错误,应用程序可通过调用WSAGetLastError()来获得特定的错误代码
参数的sockaddr 类型指针,可以直接用前面的sockaddr_in变量转换替换。该地址变量是输出参数,表示的是客户端的地址。
同时返回新的套接字,用于传输数据用。原来的套接字继续用来监听。这时候,注意3次握手的过程,还要考虑是否是阻塞,同步的状态。默认是阻塞。
accept()函数成功返回时,完成了关联的建立,即3次握手成功结束。
检测到达的连接请求:accept()函数调用成功,或者是select()函数指示监听socket上有可写数据。
5.接收数据 recv()函数
默认是阻塞函数buf缓冲区的类型都是char 最后一个变量flag 一般是0 在tcp中,要注意缓冲区不够,导致要么重发要么重收。所有要判断返回值。
int recv( _In_ SOCKET s, _Out_ char *buf, _In_ int len, _In_ int flags);
返回值:
若无错误发生,recv()返回读入的字节数。如果连接已中止,返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
6.发送数据send() 函数
int send( SOCKET s, const char FAR *buf, int len, int flags );若无错误发生,send()返回所发送数据的总数(请注意这个数字可能小于len中所规定的大小,该值应该不可以为0)。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
7.shutdown
只适合在tcp上,udp这函数没有意义。值关闭连接,不释放socket资源。shutdown会切断进程共享的套接字的所有连接,不管这个套接字的引用计数是否为零
SHUT_RD:关闭连接的读端
SHUT_WR:关闭连接的写端
SHUT_RDWR:连接的读端和写端都关闭。 这与调用shutdown两次等效。第一次调用指定SHUT_RD,第二次调用指定SHUT_WR
8.关闭closesocket ()(四次挥手)
4次握手,该函数默认是非阻塞函数。只有对一个阻塞socket,并且调用setsockopt设置了非0的超时值来使能SO_DONTLINGER。它才是阻塞的。int PASCAL FAR closesocket( SOCKET s);
如无错误发生,则closesocket()返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
在linux中关闭套接字使用的函数是close().
close把描述符的引用计数减1,仅在该计数为0时才关闭套接字,如果有其他的进程共享着这个套接字,那么它仍然是打开的,这个连接仍然可以用来读和写。
close终止读写两个方向的数据传输。
可以通过setsockopt函数设置SO_DONTLINGER 、SO_LINGER选项。
二。客户端
1创建套接字socket()
2.连接connect()函数(地址簇name要连接套接字s<服务器套接字>)
int connect(SOCKET s, const struct sockaddr * name, int namelen);s:标识一个未连接socket
name:指向要连接套接字的sockaddr 结构体的指针
namelen:sockaddr结构体的字节长度
返回值:成功则返回0, 失败返回-1.
成功后,套接字的状态从closed->syn_sent ->established.
注意阻塞函数,握手3步.connect函数由系统决定超时时间,一般是75s.
失败的多种原因:
1.具体流程是:发送一个syn,若无响应的等待几秒再发送一个,连续好几次,等到75s仍不响应。
在windows 则返回SOCKET_ERROR(也即-1).调用WSAGetLastError(),返回出错码是WSAETIMEDOUT。
在linux 则返回-1.通过全局变量errno,返回出错码是ETIMEDOUT。
2.若对客户端返回的是RST,表明在服务器主机指定的端口没有进程在等待与之连接。
出错代码是WSAECONNREFUSED(windows),ECONNREFUSED(linux)。
3.中间某个路由引发icmp错误。按照1方式联系75s发送syn包,错误码是: WSAENETUNREACH
所以需要把套接字改成非阻塞模式、并且使用select函数,自定义超时时间。可参考下面的做法。
调用的sockaddr 类型指针,可以直接用前面的sockaddr_in变量转换替换。需要知道服务端的ip地址和端口。
注意:客户端没必要调用bind进行绑定命名,主要原因是:
如果没事前调用bind()函数,connect()函数会隐式对本地socket命名,并且是任取一个端口,避免端口冲突。
同时,如果同时运行多个客户端,给套接字指定端口容易导致端口冲突。
如果调用connect失败,推荐先调用closesocket()和socket()获取一下新的socket。
补充:
TCP协议中,接收方成功接收到数据后,会回复一个ACK数据包,表示已经确认接收到ACK确认号前面的所有数据。
发送方在一定时间内没有收到服务端的ACK确认包后,就会重新发送TCP数据包。
接收方在接收到数据后,不是立即会给发送方发送ACK的。这可能由以下原因导致:
1、收到数据包的序号前面还有需要接收的数据包。因为发送方发送数据时,并不是需要等上次发送数据被Ack就可以继续发送TCP包,而这些TCP数据包达到的顺序是不保证的,这样接收方可能先接收到后发送的TCP包(注意提交给应用层时是保证顺序的)。
2、为了降低网络流量,ACK有延迟确认机制。
3、ACK的值到达最大值后,又会从0开始。接收方在收到数据后,并不会立即回复ACK,而是延迟一定时间。一般ACK延迟发送的时间为200ms,但这个200ms并非收到数据后需要延迟的时间。系统有一个固定的定时器每隔200ms会来检查是否需要发送ACK包。这样做有两个目的。
1、这样做的目的是ACK是可以合并的,也就是指如果连续收到两个TCP包,并不一定需要ACK两次,只要回复最终的ACK就可以了,可以降低网络流量。
2、如果接收方有数据要发送,那么就会在发送数据的TCP数据包里,带上ACK信息。这样做,可以避免大量的ACK以一个单独的TCP包发送,减少了网络流量。
MSS 是在建立连接时通过SYN数据包中的MSS选项里进行协商的(以太网的MTU能到1500,所以MSS可以为1460),如果没有协商,默认为536,MSS是数据净负荷,协议保证最小支持536(加上TCP和IP的头部后packet为576)
TCP中在发送的数据的ACK未回来前,能继续发送其他数据包吗
能不能发,取决于下面的条件是否满足:
1. 如果包长度达到MSS,则再根据CWND、AWND来做决定;
2. 如果该包含有FIN,则允许发送;
3. 如果没达到MSS且不包含FIN:
3.1. 设置了TCP_NODELAY选项,则允许发送;
3.2. 没设置TCP_NODELAY, 未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送(nagel算法起作用);设置了TCP_CORK选项时,需要包长度到MSS。int on = 1;
setsockopt(fd,SOL_TCP,TCP_CORK,&on,sizeof(on));
4. 上述条件都未满足,但发生了超时(一般为200ms),则立即发送
————————————————版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/baidu_16370559/article/details/104646624
二。QT的TCP连接(基于事件(异常,中断)处理)
服务器
- 创建QTcpServer对象 (相当于windows的第一步)
- 监听 (相当于windows的二三四步)
QTcpServer类的listen函数
- 等待客户端连接
- QTcpServer类的newConnection信号
- QTcpServer类的nextPendingConnection函数返回客户端QTcpSocket
通信
- 接受数据: QTcpSocket 的 readyRead 信号
QTcpSocket 的 read函数
- 发送数据: QTcpSocket 的 write函数
客户端
- 创建QTcpSocket对象 (拿到服务器的ip地址和端口号,方式:自己写入)
- QTcpSocket类的connectToHost 函数连接服务器
通过QTcpSocket类的connected信号知道已经连接到服务器了
- 通信
接受数据:QTcpSocket 的 readyRead 信号
QTcpSocket 的 read函数
发送数据:QTcpSocket 的 write函数
2.使用QT写一个TCP客户端与TCP服务器端
1.TCP服务器端
页面制作:
1.服务器端类
private slots:void on_pushButtonSend_clicked();void on_pushButtonClose_clicked();private:Ui::Widget *ui;//服务器socket套接字QTcpServer* pTcpServer;//服务器这边代表 客户端的socket套接字QTcpSocket* pTcpSocket; };
2.服务器实现
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);this->setWindowTitle("服务器端 端口9527");//创建QTcpServer对象pTcpServer=new QTcpServer(this);//指定父对象,自动回收pTcpSocket=NULL;//监听pTcpServer->listen(QHostAddress::Any,9527);//连接//客户端连接服务器--》触发(服务器端)NewConnection信号--》触发(客户端)connected信号connect(pTcpServer,&QTcpServer::newConnection,[=](){//获取客户端的socketpTcpSocket=pTcpServer->nextPendingConnection();//获取客户端端口与ipQString ipStr=pTcpSocket->peerAddress().toString();quint16 portUint=pTcpSocket->peerPort();//显示端口与ipQString buff=QString("客户端连接服务器成功:ip:%1,port:%2").arg(ipStr).arg(portUint);ui->textEditRecv->setText(buff);//只有客户端连接上服务器后,才会有此监听//触发服务器端接受数据connect(pTcpSocket,&QTcpSocket::readyRead,[=](){QByteArray data=pTcpSocket->readAll();ui->textEditRecv->append(data);});}); }
void Widget::on_pushButtonSend_clicked() {if(pTcpSocket){QString str=ui->textEditSend->toPlainText();//向客户端的套接字写数据strpTcpSocket->write(str.toUtf8().data());} }void Widget::on_pushButtonClose_clicked() {if(pTcpSocket){pTcpSocket->disconnectFromHost();//断开网络连接pTcpSocket->close();} }
2.TCP客户端
页面制作:
1.创建一个新的页面
2.页面为
3.main中添加页面,并展示
4.结果:
1.客户端类
2.客户端实现
Form::Form(QWidget *parent) :QWidget(parent),ui(new Ui::Form) {ui->setupUi(this);this->setWindowTitle("客户端");//创建socketpTcpSocket = new QTcpSocket(this);//连接服务器connect(pTcpSocket,&QTcpSocket::connected,[=](){ui->textEditRecv->setText("连接服务器成功");});//放在连接服务器内外是不是都可以?connect(pTcpSocket,&QTcpSocket::readyRead,[=](){QByteArray data = pTcpSocket->readAll();ui->textEditRecv->append(data);}); }
void Form::on_pushButtonConnect_clicked() {QString ipStr=ui->lineEditIp->text();quint16 portUint=ui->lineEditPort->text().toUInt();//客户端连接服务器--》触发(服务器端)NewConnection信号--》触发(客户端)connected信号pTcpSocket->connectToHost(ipStr,portUint); }void Form::on_pushButtonSend_clicked() {QString str=ui->textEditSend->toPlainText();pTcpSocket->write(str.toUtf8().data()); }void Form::on_pushButtonClose_clicked() {pTcpSocket->disconnectFromHost();//断开网络连接pTcpSocket->close(); }
结果:相互发送数据正常
端口是服务器设置的