根据数据传输方式的不同,基于网络协议的套接字一般分为TCP套接字和UDP套接字。TCP套接字是面向连接的,又称为基于流的套接字。
可以看出TCP/IP协议栈一共4层(实际上是7层,程序员掌握四层就够了),通过TCP套接字和UDP套接字不同的套接字经过的层级不同,各层可能通过操作系统等软件实现,也可能通过类似NIC的硬件设备实现。
见算计网络问题并非仅通过软件就能实现,编写软件前需要构建硬件系统,在此基础上通过软件实现各种算法。把这个大问题划分为若干个小问题,再逐个攻破,将大幅度提高效率。
把“通过因特网完成有效数据传输”问题按照不同领域划分成小问题之后,出现多种协议,他们通过层级结构建立了密切联系。
开放式系统
协议分为多个层次,有各种原因,当然是为了协议设计方便也是其中一项原因。
但是最重要的原因是,为了通过标准化设计开放式系统。
以多个标准为依据设计的系统称为开放式系统,其中TCP/IP协议栈也是其中之一。优点:A公司的路由器可以换成B公司的路由器,因为生产商都是按照IP层标准制造。所有的网卡制造商都遵守链路层的协议标准。标准的存在意味着科技的高速发展,软件工程的“面向对象”的诞生也有标准化的影子。
链路层就是物理链接领域标准化的结果。两台主机之间进行数据交互需要物理链接,链路层就负责这些标准。
物理连接好之后,在复杂的网络中传输数据,首先考虑的就是路径问题,选择哪条路径就是IP层的工作。IP本身是面向消息的、不可靠的协议。如果传输发生路径错误则选择其他路径;但是如果发生数据丢失或错误,就无法解决。IP协议无法应对数据错误。
IP层解决数据传输中的路径问题。TCP和UDP层以IP层提供的路径信息为基础完成实际的数据传输,所以该层又称为传输层。
IP层只关注一个数据包的传输过程。传输的顺序并不可可靠。若只利用IP层传输数据,则可能导致后传输的数据报B比先传输的数据报A先到达。
如果在数据交换过程中可以确认对方已收到数据,并重传丢失的数据,那么即使IP层不保证数据传输,这类通信也是可靠的。
总之TCP和UDP存在于IP层之上,决定主机间的数据传输方式,
TCP协议确认后向不可靠的IP协议赋予可靠性。
上述内容都是套接字通信过程中自动处理的。选择数据传输路径、数据确认过程,都被
隐藏到套接字内部。编写软件过程中,需要根据程序特点决定服务器和客户端之间的数据传输规则,这便是应用层协议。网络编程大部分内容就是设计并实现应用层协议。
前两个阶段之前已经讨论过。
接下来就讨论调用listen函数进入等待连接请求状态。只有调用了listen函数,客户端才能发出连接请求。换而言之,这时客户端才能调用connect函数(提前调用会发生错误)
#include<sys/socket.h>
int listen(int sock,int backlog);
成功返回0,失败返回-1
sock 希望进入等待连接请求状态的的套接字文件描述符,传递的描述符套接字参数成为服务端套接字(监听套接字)。
backlog 连接请求等待队列(Queue)的长度,若为5,则队列长度为5,表示最多使5个连接请求进入队列。
等待连接请求状态和连接请求等待队列
“服务器处于等待连接请求状态”是指:客户端请求连接时,受理连接前一直使请求处于等待状态。(个人理解就是,服务器这时等待客户端的连接请求)。
客户端的连接请求是一种数据,想要接收就要套接字。这个任务就是服务器套接字完成。服务端套接字是接收连接请求的门卫或者门。
服务器端向服务器申请连接,如果服务器繁忙,服务端套接字则将连接请求放进连接请求等待队列。服务端套接字是门卫。
listen函数就可以生成这种门卫(服务端套接字),第二个参数决定了连接请求等待队列的大小。
准备好服务端套接字和连接请求等待队列后,这种处于可接受连接请求的状态称为等待连接请求状态。
调用listen后,如果有新的连接请求,应该按序受理。受理连接请求(进入接受数据的状态)需要套接字,但是服务器端套接字作为门卫不能作为数据交换的套接字。因此需要一个新的套接字,但不需要自己创建。accept函数会自动创建。
#include<sys/socket.h>
int accept(int sock,struct sockaddr * addr,socklen_t * addrlen);
成功创建返回套接字文件描述符,失败返回-1;
sock 服务端套接字的文件描述符
addr 保存发起连接请求的客户端地址信息的变量地址,调用函数后向传递来的
地址变量参数填充客户端地址信息。
addrlen 第二个参数addr结构体的长度,但是存有长度的变量地址。函数调用完后,
该变量即被填入客户端地址长度。
accept函数受理连接请求等待队列的客户端连接请求。调用成功时,accept函数内部将产生用于数据I/O的套接字,并返回文件描述符。套接字是自动创建的,并自动与发起连接请求的客户端连接。
accept函数会在连接请求等待队列的队头取一个连接请求与客户端建立连接,并返回创建的套接字文件描述符。如果等待队列为空,则accept函数不会返回,直到队列出现新的连接请求。
与服务器相比,区别就在“请求连接”,它是创建客户端套接字后向服务端发起连接请求。
服务端调用完listen函数后创建连接请求等待队列,之后客户端就能请求连接。
#include<sys/socket.h>
int connect(int sock,struct sockaddr * servaddr,socklen_t addrlen);
sock 客户端套接字文件描述符
servaddr 保存目标服务器端地址信息的变量地址值
addrlen 以字节为单位传递已传递给第二个结构体参数servaddr的地址变量长度
客户端调用connect函数之后,发生以下情况之一才返回:
服务器端接收连接请求
发生断网等异常情况而中断连接请求
接受数据并不代表立即调用accept函数,其实是服务器端把连接请求信息记录到等待队列。因此connect函数返回后并不立即进行数据交换。 客户端套接字何时何地分配地址呢?
何时?调用connect函数时
何地?操作系统,更准确地说是内核中
如何?IP用主机IP,端口随机分配
客户端只能等到服务器端调用listen函数后才能调用connect函数。
客户端调用connect函数之前,服务端可能率先调用accept函数。
此时accept函数处于阻塞转态,直到客户端调用connect函数为止。
顾名思义,服务器端将客户端传输的字符串数据原封不动的传回客户端,就像回声一样。
之前的Hello World服务端处理完一个客户端连接请求后,就退出了,所以连接请求等待队列没太大意义。要想向所有用户提供服务。要插入循环反复调用accept函数 迭代服务器函数调用顺序
调用完accept函数之后,紧接着调用I/O有关的函数read,write,然后调用close函数。
这并非针对服务端套接字,而是针对accept函数调用时创建的套接字(用于与客户端数据交换的套接字)。
调用close函数意味着结束了针对某一客户端的服务。如果还想服务其他客户端,就要重新调用accept函数。
服务端同一时刻只与一个客户端相连,并提供回声服务
服务端依次向5个客户端提供服务并退出
客户端接收用户输入的字符串并发往服务器端
服务器端将接收到的字符串传回客户端,即“回声”
服务端与客户端之间的字符串回声一直执行到客户端输入Q为止
服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char message[BUF_SIZE];
int str_len, i;
struct sockaddr_in serv_adr;
struct sockaddr_in clnt_adr;
socklen_t clnt_adr_sz;
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock==-1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
clnt_adr_sz=sizeof(clnt_adr);
for(i=0; i<5; i++)
{
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
if(clnt_sock==-1)
error_handling("accept() error");
else
printf("Connected client %d \n", i+1);
while((str_len=read(clnt_sock, message, BUF_SIZE))!=0)
write(clnt_sock, message, str_len);
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_adr;
if(argc!=3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0);
if(sock==-1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("connect() error!");
else
puts("Connected...........");
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
write(sock, message, strlen(message));
str_len=read(sock, message, BUF_SIZE-1);
message[str_len]=0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
write(sock, message, strlen(message));
str_len=read(sock, message, BUF_SIZE-1);
message[str_len]=0;
printf("Message from server: %s", message)
错误假设:每次调用read和write函数都会以字符串为单位执行实际的IO操作。
每次调用write函数都会传递一个字符串,所以这种假设某种程度算合理。
之前说过,TCP不存在数据边界。以上客户端是基于TCP的,因此多次调用write函数传递的字符串有可能一次性传递到服务端。
此时客户端可能从服务端接收到多个字符串组成的字符串,这不是我们希望的。
服务端可能的情况:字符串太长,要分两个数据包发送
服务端希望通过一个write函数传递数据,但是数据太大,操作系统可能会把数据分成多个数据包发送给客户端。
客户端可能在没有收到所有数据时就调用了read函数。
以上问题将会在下一章解决
思路基本和Linux一样需要注意的几点:
通过WSAStartup、WSACleanup初始化和清除套接字相关库
把数据类型和变量名切换为Windows风格
数据传输中用send和recv函数
关闭套接字用closesocket函数
服务端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF_SIZE 1024
void ErrorHandling(char *message);
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
char message[BUF_SIZE];
int strLen, i;
SOCKADDR_IN servAdr, clntAdr;
int clntAdrSize;
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
ErrorHandling("WSAStartup() error!");
hServSock=socket(PF_INET, SOCK_STREAM, 0);
if(hServSock==INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family=AF_INET;
servAdr.sin_addr.s_addr=htonl(INADDR_ANY);
servAdr.sin_port=htons(atoi(argv[1]));
if(bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr))==SOCKET_ERROR)
ErrorHandling("bind() error");
if(listen(hServSock, 5)==SOCKET_ERROR)
ErrorHandling("listen() error");
clntAdrSize=sizeof(clntAdr);
for(i=0; i<5; i++)
{
hClntSock=accept(hServSock, (SOCKADDR*)&clntAdr, &clntAdrSize);
if(hClntSock==-1)
ErrorHandling("accept() error");
else
printf("Connected client %d \n", i+1);
while((strLen=recv(hClntSock, message, BUF_SIZE, 0))!=0)
send(hClntSock, message, strLen, 0);
closesocket(hClntSock);
}
closesocket(hServSock);
WSACleanup();
return 0;
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF_SIZE 1024
void ErrorHandling(char *message);
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hSocket;
char message[BUF_SIZE];
int strLen;
SOCKADDR_IN servAdr;
if(argc!=3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
ErrorHandling("WSAStartup() error!");
hSocket=socket(PF_INET, SOCK_STREAM, 0);
if(hSocket==INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family=AF_INET;
servAdr.sin_addr.s_addr=inet_addr(argv[1]);
servAdr.sin_port=htons(atoi(argv[2]));
if(connect(hSocket, (SOCKADDR*)&servAdr, sizeof(servAdr))==SOCKET_ERROR)
ErrorHandling("connect() error!");
else
puts("Connected...........");
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
send(hSocket, message, strlen(message), 0);
strLen=recv(hSocket, message, BUF_SIZE-1, 0);
message[strLen]=0;
printf("Message from server: %s", message);
}
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
本文章使用limfx的vscode插件快速发布