TCP(The Transmission Control Protocol):传输控制协议
UDP/TCP协议都属于传输层协议,都位于IP协议以上,将UDP/TCP数据报封装于IP数据内传输。
UDP首部:包含源端口,目标端口等数据。端口保证数据能准确传输到指定的进程。
IP协议是不可靠协议,UDP本身没有任何确保可靠的措施,故UDP协议也是不可靠协议。UDP协议适用于对数据可靠性,顺序交付无要求的程序,UDP能提供更快,更小消耗的传输服务。
TCP首部:相比UDP,TCP同样包含源端口,目标端口等数据的同时,TCP还包含序号,确认号等信息,这些数据用于确认数据是否被完整交付,TCP是可靠协议的原因就在这里。
TCP虽然建立在不可靠协议IP之上,但TCP采用了多种机制,确保数据有序,可靠的交付。TCP多用于对数据安全要求较高的应用,如Web,电子邮件等。
套接字的概念
套接字,是操作系统内核中的一个数据结构。它是网络中的节点进行相互通信的门户。它是网络进程的ID。
网络通信,归根究竟还是进程间的通信(不同计算机上的进程间通信)。在网络中。每个节点(计算机或路由)都有一个网络地址。也就是IP地址。
两个进程通信时,首先要确定各自所在的网络节点的网络地址。可是,网络地址仅仅能确定进程所在的计算机,而一台计算机上非常可能同一时候执行着多个进程,所以仅凭网络地址还不能确定究竟是和网络中的哪一个进程进行通信。因此套接口中还须要包含其它的信息。也就是port号(PORT)。在一台计算机中,一个port号一次仅仅能分配给一个进程,也就是说,在一台计算机中,port号和进程之间是一一相应关系。
所以,使用port号和网络地址的组合能够唯一的确定整个网络中的一个网络进程。
网络编程也称为socket编程,socket通常译作”套接字“,为一套网络编程的接口API,但原意其实意译应该为”接口“。也就是操作系统提供给开发人员进行网络开发的API接口。这套接口通常可以通过参数的调整支持多种协议,包括TCP、UDP和IP等等。
从编码角度看,源IP地址和目的IP地址以及源端口号和目的端口号的组合称为套接字。其用于标识客户端请求的服务器和服务。
流套接字(SOCK_STREAM)------>使用TCP协议,能够实现可靠的数据服务,提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复发送,并按顺序接收。
数据报套接字(SOCK_DGRAM)------>使用UDP协议,提供无连接的服务,无法保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。
原始套接字(SOCK_RAW)------>允许对较低层次的协议直接访问,比如IP、 ICMP协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。比如,我们可以通过RAW SOCKET来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包。网络监听技术很大程度上依赖于SOCKET_RAW。
应用编程接口即:调用一些操作系统提供的api,来与应用进程进行交互。而socket api是一套与网络通讯相关的、操作系统提供给应用开发的编程接口,如:微软公司在其操作系统中采用了套接字接口 API ,形成了一个稍有不同的 API,并称之为Windows Socket Interface或WINSOCK。
使用Socket的应用程序在使用Socket之前必须首先调用 WSAStartup函数,两个参数:
第一个参数指明程序请求使用的WinSock版本,其中高位字节指明副版本、低位字节指明主版本.十六进制整数,例如0x102表示2.1版。
第二个参数指向WSADATA结构的指针
返回实际的WinSock的版本信息。
应用程序在完成对请求的Socket库的使用,最后要调用WSACleanup函数解除与Socket库的绑定并释放Socket库所占用的系统资源 。不清理的话,在运行codeblocks的时候,常常可能会发生id.exe被占用错误。无法运行,需要去管理器找进程关掉才可以继续运行。
socket函数:sd = socket(protofamily,type,proto);
创建套接字,操作系统返回套接字描述符(sd)
第一个参数(协议族): protofamily = PF_INET(TCP/IP)
第二个参数(套接字类型): type = SOCK_STREAM,SOCK_DGRAM or SOCK_RAW(TCP/IP)
第三个参数(协议号):0为默认
1、首先,Winsock API 函数由WS2_32.DLL支持,可通过WS2_32.LIB访问。故Windows socket编程前需要加载ws2_32.lib,然后初始化WS2_32.DLL,通过函数WSAStartup完成初始化。一般程序最后需要终止DLL使用,此时需要调用WSACleanup函数。
ws2_32.dll是Windows Sockets应用程序接口, 用于支持Internet和网络应用程序。
DLL 是一个库,其中包含可同时由多个程序使用的代码和数据。对于Windows,操作系统的很多功能都由 DLL 提供。
//以下是加载库,以及初始化
#include
#include
#pragma comment (lib,"ws2_32.lib"); //加载ws2_32.lib
int main()
{
//1、ws2_32.DLL初始化
WSADATA wsaData;
/*
* socket编程中:
声明调用不同的Winsock版本。
例如MAKEWORD(2,2)就是调用2.2版,MAKEWORD(1,1)就是调用1.1版。
*/
WSAStartup(MAKEWORD(2,2),&wsaData); //初始化
/*
*中间代码
*/
WSACleanup(); //终止DLL使用
}
2、随后,创建socket套接字。
//2、创建套接字
/*
* typedef UINT_PTR SOCKET;
* typedef unsigned int UINT_PTR;
* 在32位操作系统里,一个unsigned int是4个字节。64位操作系统上,一个unsigned int是8个字节。所以用UINT_PTR代替int理论上可以让代码具有更好的移植性,当然也让代码看起来更专业.
* socket(int domain, int type, int protocol);
* int domain参数表示套接字要使用的协议簇,网络编程一般使用AF_INET宏,AF_INET(TCP/IP – IPv4)
* type参数指的是套接字类型,SOCK_STREAM(TCP流),SOCK_DGRAM(UDP数据报)
* protocol表示协议,使用AF_INET簇,TCP连接时,设为IPPROTO_TCP。
*/
SOCKET serverSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
3、创建套接字之后,绑定套接字.
Socket套接字基于计算机网络,提供同一系统上不同进程或由局域网连接在一起的不同机器上的进程间通讯功能。如下图:
套接字通过IP地址、Port端口号标识,通过这个标识可以在整个局域网定位一个套接字,通过套接字进程便可以相互传输数据。而这就是绑定套接字所做的工作,即为每一个套接字绑定相应的IP地址和端口号标识。
//3、绑定套接字
/*
* struct sockaddr_in这个结构体用来处理网络通信的地址。
在各种系统调用或者函数中,只要和网络地址打交道,就得用到这两个结构体。网络 中的地址包含3个方面的属性:
1 地址类型; 2 ip地址; 3 端口
*/
SOCKADDR_IN sockAddr;
/*
* memset 函数是内存赋值函数,用来给某一块内存空间进行赋值的。(对一片内存空间逐字节进行初始化)
* 其原型是:void* memset(void *_Dst, int _Val, size_t _Size)
* _Dst是目标起始地址,_Val是要赋的值,_Size是要赋值的字节数
*/
memset(&sockAddr,0,sizeof(sockAddr)); //初始化
sockAddr.sin_family = AF_INET; //地址类型
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //IP地址(127.0.0.1是回送地址,指本机)
sockAddr.sin_port = htons(9080); //端口号 htons函数:将主机字节顺序转化为网络字节顺序
bind(serverSock,(SOCKADDR*)&sockAddr,sizeof(SOCKADDR));
关于SOCKADDR_IN和SOCKADDR在另外文档中有详解。
bind函数将socket与协议、IP和端口号绑定起来,相对于给socket“命名”唯一的标识,这样其他的进程就可以通过这个标识找到这个socket。
//关于bind函数
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
//函数作用:将本地地址与套接字关联起来。
//函数参数:
// (1)参数 sockfd ,需要绑定的socket。
// (2)参数 addr ,存放了服务端用于通信的地址和端口。
// (3)参数 addrlen ,表示 addr 结构体的大小
//返回值:成功则返回0 ,失败返回-1,错误原因存于 errno 中。如果绑定的地址错误,或者端口已被占用,bind 函数一定会报错,否则一般不会返回错误。
//关于inet_addr函数
unsigned long WSAAPI inet_addr(const char *cp);
//功能:inet_addr函数将包含 IPv4 点分十进制地址的字符串转换为 IN_ADDR 结构的 正确 地址
//参数:cp代表点分十进制的IP地址,如1.2.3.4
//返回值:
/*如果没有发生错误,inet_addr函数将返回一个无符号长整型值,其中包含给定 Internet 地址的合适二进制表示。
如果cp参数中的字符串不包含合法的 Internet 地址,例如,如果“abcd”地址的一部分超过 255,则 inet_addr返回值INADDR_NONE。
*/
4、进入监听状态
//4、进入监听状态
listen(serverSock,20);
int listen(SOCKET s, int nQueueSize);
第一个参数: 监听的socket。
第二个参数: 套接字监听队列最大连接请求数。
该函数将监听对socket的连接请求。
5、
//5、定义接受客户端请求的数据结构变量
SOCKADDR cltAddr;
int nSize = sizeof(cltAddr);
//buffer是一块共用的内存,刚开始接收的数据放在这里面,给客户端回复消息也用的这块内存
char buffer[BUF_SIZE] = { 0 };
while (1)
{
//6、阻塞直到客户端发来消息,创建客户socket
SOCKET clientSock = accept(serverSock,(SOCKADDR*)&cltAddr,&nSize);
//7、接受客户端数据
int strLen = recv(clientSock, buffer, BUF_SIZE, 0); //数据存放在buffer这个字符串数组中
printf("Message form client: %s\n", buffer);
//8、给客户端回复消息
printf("Input a string:\n");
getss(buffer,BUF_SIZE);
send(clientSock, buffer, strLen, 0);
//9、关闭客户端套接字
closesocket(clientSock);
//10、重置缓冲区
memset(buffer,0,BUF_SIZE);
}
SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen);
第一个参数: socket为被监听的socket,即服务端socket
第二个参数: 对应AF_INET,一个sockaddr指针,将写入发送请求方的sockaddr_in信息,即客户端的sockaddr_in信息。
第三个参数: 对应AF_INET,sockaddr结构体的大小。
该函数用于接受一个socket连接请求,返回一个新的连接socket(可以理解为客户端的socket),发送与接收数据通过这个连接socket。
int send(SOCKET s, const char *buf, int len, int flags );
第一个参数: socket为对方的socket。
第二个参数: 发送数据的缓冲区。
第三个参数: 数据缓冲区大小。
第四个参数: 紧急状态,一般值为0。
该函数用于向对方socket发送数据,成功返回发送数据的大小数。
int recv(SOCKET s, char *buf, int len, int flags);
第一个参数: socket为对方的socket。
第二个参数: 接收数据的缓冲区。
第三个参数: 缓冲区大小。
第四个参数: 紧急状态,一般为0。
该函数用于接收对方发送的数据,成功返回发送数据的大小数。
最后关闭服务端套接字、终止DLL使用即可
服务器端的源码:
#include
#include
#pragma comment (lib,"ws2_32.lib"); //加载ws2_32.lib
#pragma warning (disable:4996); //解决C4996 'inet_addr'错误
#define BUF_SIZE 100
/*
* char* getss(char* str, int num)
{
if (fgets(str, num, stdin) != 0)
{
//size_t是标准C库中定义的,在64位系统中为long long unsigned int
size_t len = strlen(str);
if (len > 0 && str[len - 1] == 'n')
str[len - 1] = ' ';
return str;
}
}
*/
int main()
{
//1、ws2_32.DLL初始化
WSADATA wsaData;
/*
* socket编程中:
声明调用不同的Winsock版本。
例如MAKEWORD(2,2)就是调用2.2版,MAKEWORD(1,1)就是调用1.1版。
*/
WSAStartup(MAKEWORD(2,2),&wsaData); //初始化
//2、创建套接字
/*
*socket(int domain, int type, int protocol);
* 其中 “int domain”参数表示套接字要使用的协议簇 AF_INET(TCP/IP – IPv4)
* “type”参数指的是套接字类型,SOCK_STREAM(TCP流),SOCK_DGRAM(UDP数据报)
* protocol”一般设置为“0
*/
SOCKET serverSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//3、绑定套接字
/*
* struct sockaddr_in这个结构体用来处理网络通信的地址。
在各种系统调用或者函数中,只要和网络地址打交道,就得用到这两个结构体。网络中的地址包含3个方面的属性:
1 地址类型; 2 ip地址; 3 端口
*/
SOCKADDR_IN sockAddr;
/*
* memset 函数是内存赋值函数,用来给某一块内存空间进行赋值的。(对一片内存空间逐字节进行初始化)
* 其原型是:void* memset(void *_Dst, int _Val, size_t _Size)
* _Dst是目标起始地址,_Val是要赋的值,_Size是要赋值的字节数
*/
memset(&sockAddr,0,sizeof(sockAddr)); //初始化
sockAddr.sin_family = AF_INET; //地址类型
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //IP地址(127.0.0.1是回送地址,指本机)
sockAddr.sin_port = htons(9080); //端口号 htons函数:将主机字节顺序转化为网络字节顺序
bind(serverSock,(SOCKADDR*)&sockAddr,sizeof(SOCKADDR));
//4、进入监听状态
listen(serverSock,20);
printf("服务器进入监听状态...\n");
//5、定义接受客户端请求的数据结构变量
SOCKADDR cltAddr;
int nSize = sizeof(cltAddr);
char buffer[BUF_SIZE] = { 0 };
int num = 0;
while (1)
{
//6、阻塞直到客户端发来消息,创建客户socket
SOCKET clientSock = accept(serverSock,(SOCKADDR*)&cltAddr,&nSize);
if(!num)
printf("连接成功\n");
//7、接受客户端数据
int strLen = recv(clientSock, buffer, BUF_SIZE, 0);
printf("Message form client: %s\n", buffer);
//8、给客户端回复消息
printf("Input a string to client:");
// getss(buffer, sizeof(buffer));
gets(buffer);
send(clientSock, buffer, strLen, 0);
//9、关闭客户端套接字
closesocket(clientSock);
//10、重置缓冲区
memset(buffer,0,BUF_SIZE);
num++;
}
//11、关闭服务端套接字
closesocket(serverSock);
//12、终止DLL使用
WSACleanup();
}
注:目前GCC中还没有完全实现此标准, 因此 gets_s() 函数尚未包含在目前的GNU 工具链中,因此调用gets_s函数出错,这里干脆写了一个getss函数。
这是老师写的getss函数,我感觉直接用gets函数都可以。
关于fgets函数:
char *fgets(char *buf, int bufsize, FILE *stream);
参数
*buf: 字符型指针,指向用来存储所得数据的地址。
bufsize: 整型数据,指明存储数据的大小。
*stream: 文件结构体指针,将要读取的文件流。
返回值
成功,则返回第一个参数buf;
在读字符时遇到end-of-file,则eof指示器被设置,如果还没读入任何字符就遇到这种情况,则buf保持原来的内容,返回NULL;
如果发生读入错误,error指示器被设置,返回NULL,buf的值可能被改变。
客户端跟服务端类似,源码如下:
#include
#include
#pragma comment(lib,"ws2_32.lib")
#pragma warning(disable:4996)
#define BUF_SIZE 100
/*
char* getss(char* str, int num)
{
if (fgets(str, num, stdin) != 0)
{
size_t len = strlen(str);
// if (len > 0 && str[len - 1] == 'n')
// str[len - 1] = ' ';
return str;
}
return 0;
}
*/
int main()
{
//初始化DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建套接字
SOCKADDR_IN sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr));
sockAddr.sin_family = AF_INET;
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
sockAddr.sin_port = htons(9080);
char bufSend[BUF_SIZE] = { 0 };
char bufRecv[BUF_SIZE] = { 0 };
while (1)
{
// 3、创建客户端套接字
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 4、连接服务器
connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
//5、获取用户输入的字符串发送给服务器
printf("Input a string:");
gets(bufSend);
// getss(bufSend, sizeof(bufSend));
send(sock, bufSend, BUF_SIZE, 0);
//6、接受服务器传回来的消息
recv(sock, bufRecv, BUF_SIZE, 0);
printf("Message form server: %s\n",bufRecv);
//7、重置缓冲区
memset(bufSend, 0, BUF_SIZE);
memset(bufRecv, 0, BUF_SIZE);
// 8、关闭套接字
closesocket(sock);
}
// 9、终止DLL使用
WSACleanup();
return 0;
}
注意connect函数即可:
int connect(SOCKET s,const struct sockaddr *saddr,int namelen) ;
第一个参数: socket本地进程的socket。
第二个参数: 对应AF_INET,对方IP,端口等socket地址标识sockaddr_in。
第三个参数: 对应AF_INET,使用sockaddr_in结构大小。
#pragma指令的作用是:用于指定计算机或操作系统特定的编译器功能。C 和 C++ 的每个实现均支持某些对其主机或操作系统唯一的功能。
大家应该都知道:指定该文件在编译源代码文件时仅由编译器包含(打开)一次。
使用 #pragma once 可减少生成次数,和使用预处理宏定义来避免多次包含文件的内容的效果是一样的,但是需要键入的代码少,可减少错误率。
#pragma waring(…)
启用编译器警告消息的行为和选择性修改。
#pragma warning( disable : 4507 34; once : 4385; error : 164 ) //这1行跟下面3行效果一样
#pragma warning( disable : 4507 34 ) //不发出4507和34警告,即有4507和34警告时不显示
#pragma warning( once : 4385 ) //4385警告信息只报告一次
#pragma warning( error : 164 ) //把164警告信息作为一个错误
#pragma comment(comment-type [,“commentstring”])
该指令将一个注释记录放入一个对象文件或可执行文件中。
comment-type 是一个预定义的标识符(如下所述,一共5个),它指定了注释记录的类型。 可选 commentstring 是一个字符串,它提供了某些注释类型的附加信息。 由于 commentstring 是一个字符串,因此它遵循有关转义字符、嵌入的引号 (") 和串联的字符串的所有规则。
compiler
将编译器的名称和版本号置于对象文件中。 此注释记录将被链接器忽略。 如果为此记录类型提供 commentstring 参数,则编译器会生成警告。
exestr
将 commentstring 置于对象文件中。 在链接时,会将该字符串置于可执行文件内。 加载可执行文件时,不会将字符串加载到内存中;但是,可以使用在文件中查找可打印字符串的程序来找到它。 此注释记录类型的一个用途是将版本号或类似信息嵌入可执行文件中。
linker
将链接器选项置于对象文件中。 可以使用注释类型来指定链接器选项,而不是将其传递到命令行或在开发环境中指定它。
user
将一般注释置于对象文件中。 commentstring 参数包含注释文本。 此注释记录将被链接器忽略。
lib(这个最常用了)
将库搜索记录置于对象文件中。 此注释类型必须带有包含您希望链接器搜索的库的名称(和可能的路径)的 commentstring 参数。 库名称遵循对象文件中的默认库搜索记录;链接器会搜索此库,这就像在命令行上对其命名一样,前提是未使用 /nodefaultlib 指定库。 可以将多个库搜索记录置于同一个源文件中;各个记录将以其在源文件中显示的顺序出现在对象文件中。
如果默认库和添加的库的顺序很重要,则使用 /Zl 开关进行编译会阻止将默认库名称置于对象模块中。 然后,可使用另一个注释指令在添加的库的后面插入默认库的名称。 与这些指令一起列出的库将以其在源代码中的发现顺序出现在对象模块中。
#pragma message(messageString)
不中断编译的情况下,发送一个字符串文字量到标准输出。message编译指示的典型运用是在编译时显示信息。
MAKEWORD是将两个byte型合并成一个word型,一个在高8位(b),一个在低8位(a)
比如a=2;b=1
2的二进制是00000010,1的二进制为00000001,B是表示高8位,A表示低8位 合并起来就是100000010
int WSAAPI connect(
SOCKET s,
const struct sockaddr FAR * name,
int namelen
);
s
标识未连接套接字的描述符。
name
指向应建立连接的sockaddr结构的指针。
namelen
name参数指向的sockaddr结构的长度(以字节为单位)。