SOCKET:
(有些内容摘自网络)
是应用层与TCP/IP协议通信的中间软件抽象层,就是一组接口。
两种常用套接字
流格式套接字(SOCK_STREAM)【使用了 TCP 协议】
是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。
○ 数据在传输过程中不会消失;
○ 数据是按照顺序传输的;
○ 数据的发送和接收不是同步的
数据报格式套接字(SOCK_DGRAM)【使用了 UDP 协议】
计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。
○ 强调快速传输而非传输顺序;
○ 传输的数据可能丢失也可能损毁;
○ 限制每次传输的数据大小;
○ 数据的发送和接收是同步的
TCP/IP
应用层:
向用户提供一组常用的应用程序,比如电子邮件、文件传输访问、远程登录等。远程登录TELNET使用TELNET协议提供在网络其它主机上注册的接口。TELNET会话提供了基于字符的虚拟终端。文件传输访问FTP使用FTP协议来提供网络内机器间的文件拷贝功能。
传输层:
提供应用程序间的通信。其功能包括:一、格式化信息流;二、提供可靠传输。为实现后者,传输层协议规定接收端必须发回确认,并且假如分组丢失,必须重新发送。
网络层 :
负责相邻计算机之间的通信。其功能包括三方面。
一、处理来自传输层的分组发送请求,收到请求后,将分组装入IP数据报,填充报头,选择去往信宿机的路径,然后将数据报发往适当的网络接口。
二、处理输入数据报:首先检查其合法性,然后进行寻径–假如该数据报已到达信宿机,则去掉报头,将剩下部分交给适当的传输协议;假如该数据报尚未到达信宿,则转发该数据报。
三、处理路径、流控、拥塞等问题。
网络接口层:
这是TCP/IP软件的最低层,负责接收IP数据报并通过网络发送之,或者从网络上接收物理帧,抽出IP数据报,交给IP层。
如何找到通信的目标(找到某一台计算机地址的某个网络服务)
找到某一台计算机上的某个网络服务应用程序,必须具备IP地址、MAC地址、端口号;
- IP 地址只能定位到一个局域网,无法定位到具体的一台计算机
- MAC地址可以定位到某一局域网的某台计算机,每台计算机的MAC地址是一定的;
- 最后得到某台计算机上的网络服务应用程序的端口,为了区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号(Port Number);
实现通信
三次握手
在socket编程中,这一过程由客户端执行connect来触发。
- 第一次握手:客户端将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给服务器端,客户端进入SYN_SENT状态,等待服务器端确认。
- 第二次握手:服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。
- 第三次握手:客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。
四次挥手
需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发,整个流程如下图所示:
由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭,上图描述的即是如此。
- 第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
- 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
- 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
- 四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
实现通信程序(基于WINDOWS)
服务端程序
建议IP地址转换函数inet_pton;
#include <stdio.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
#include <Ws2tcpip.h>
#include <iostream>
int main() {
//初始化 DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建套接字
SOCKET servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
//绑定套接字
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET; //使用IPv4地址
inet_pton(AF_INET, "127.0.0.1", &sockAddr.sin_addr.s_addr);
sockAddr.sin_port = htons(1234); //端口
bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
//进入监听状态
listen(servSock, 20);
//接收客户端请求
SOCKADDR clntAddr;
int nSize = sizeof(SOCKADDR);
SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
//向客户端发送数据
char str[128]= "bdm!";
send(clntSock, str, strlen(str) + 1, NULL);
//关闭套接字
closesocket(clntSock);
closesocket(servSock);
//终止 DLL 的使用
WSACleanup();
return 0;
}
客户端程序
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#include <Ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib") //加载 ws2_32.dll
int main() {
//初始化DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建套接字
SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
//向服务器发起请求
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET;
inet_pton(AF_INET, "127.0.0.1", &sockAddr.sin_addr.s_addr);
sockAddr.sin_port = htons(1234);
connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
//接收服务器传回的数据
char szBuffer[128] = { 0 };
recv(sock, szBuffer, MAXBYTE, NULL);
//输出接收到的数据
printf("Message form server: %s\n", szBuffer);
//关闭套接字
closesocket(sock);
//终止使用 DLL
WSACleanup();
system("pause");
return 0;
}
服务器端持续监听客户端的请求:
服务端
#include <stdio.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
#include <Ws2tcpip.h>
#include <iostream>
using namespace std;
int main() {
//初始化 DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建套接字
SOCKET servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
//绑定套接字
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET; //使用IPv4地址
inet_pton(AF_INET, "127.0.0.1", &sockAddr.sin_addr.s_addr);
sockAddr.sin_port = htons(1234); //端口
bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
//进入监听状态
listen(servSock, 20);
//接收客户端请求
SOCKADDR clntAddr;
int nSize = sizeof(SOCKADDR);
/*char *str = (char*)"bdm!";*/
SOCKET clntSock = INVALID_SOCKET;
clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
if (clntSock== INVALID_SOCKET)
{
printf("no value client");
}
printf("new service join");
char _recvbuf[128] = {};
while (true)
{
//接受客户端的请求数据
int nLen = recv(clntSock, _recvbuf, 128, 0);
if (nLen <= 0) {
printf("client go-out");
break;
}
else {
//处理请求
if (0 == strcmp(_recvbuf, "getName")) {
char str[128] = "uzqi!";
send(clntSock, str, strlen(str) + 1, 0);
}
else if(0 == strcmp(_recvbuf, "getAge"))
{
char str[128] = "80";
send(clntSock, str, strlen(str) + 1, 0);
}
else
{
char str[128] = "你是谁";
;
//向客户端发送数
send(clntSock, str, strlen(str) + 1, 0);
}
}
}
//关闭套接字
closesocket(clntSock);
closesocket(servSock);
//终止 DLL 的使用
WSACleanup();
return 0;
}
客户端
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#include <Ws2tcpip.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib") //加载 ws2_32.dll
using namespace std;
int main() {
//初始化DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建套接字
SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
//向服务器发起请求
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET;
inet_pton(AF_INET, "127.0.0.1", &sockAddr.sin_addr.s_addr);
sockAddr.sin_port = htons(1234);
int ret = connect(sock, (SOCKADDR*)&sockAddr, sizeof(sockaddr_in));
if (SOCKET_ERROR==ret)
{
printf("error to connect ");
}
else
{
printf("succesd to connect ");
}
while (true)
{
//输入请求
char cmdbuf[128];
scanf("%s",cmdbuf);
//处理请求
if (0 == strcmp(cmdbuf, "exit")) {
cout << "sadf" << 2<<endl;
break;
}
else
{
//向服务器发送请求
send(sock, cmdbuf, strlen(cmdbuf) + 1, 0);
}
//接收服务器传回的数据
char szBuffer[128] = {};
recv(sock, szBuffer, 128, 0);
//输出接收到的数据
printf("Message form server: %s\n", szBuffer);
}
接收服务器传回的数据
//char szBuffer[128] = {};
//recv(sock, szBuffer, 128, 0);
//cout << " szBuffer " << szBuffer << endl;
输出接收到的数据
//printf("Message form server: %s\n", szBuffer);
//关闭套接字
closesocket(sock);
//终止使用 DLL
WSACleanup();
system("pause");
return 0;
}
回声传播:
https://blog.csdn.net/w00347190/article/details/100169448
SOCKET阻塞模式
所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。
-
accept在阻塞模式下,没有新连接时,线程会进入睡眠状态;非阻塞模式下,没有新连接时,立即返回WOULDBLOCK错误。
-
connect在阻塞模式下,仅TCP连接建立成功或出错时才返回,分几种具体的情况,这里不再叙述;非阻塞模式下,该函数会立即返回INPROCESS错误(需用select检测该连接是否建立成功)
-
recv/recvfrom/send/sendto这两类函数读写收/发送缓冲区。
- 当发送数据时,首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么收发数据 会被阻塞(暂停执行),一直会等到缓冲区中的数据被发送到目标机器,腾出足够的空间为止
- 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定
- 如果要写入的数据大于缓冲区的最大长度,那么将分批写入
- 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到再次读取。
-
select/poll/epoll并不是真正意义上的阻塞,它们的阻塞是由于它们最后一个timeout参数决定的,timeout大于0时,它们会一直等待直到超时才退出(相等于阻塞了吧,_),而timeout=-1即永远等待