- Socket是针对端系统,也就是用户主机上开发程序,不涉及网络设备(交换机、路由器)
- 独立于网卡驱动层之上,不涉及硬件,即基于Packet Driver编程
- 端:是指通信双方两台电脑
- 应用编程接口API 也就是两端 应用层内部的应用进程之间的 数据通信,遵循应用层协议,他们之间数据通信需要底层(传输层、网络层、数据链路层、物理层)的支持,底层一般是涉及到操作系统的知识
- 应用进程 和 操作系统 之间需要一个接口,这个接口就是应用编程接口 API,这个接口API就是应用进程的控制权和操作系统的控制权之间进行转换的一个系统调用的接口。即应用进程通过API接口将控制权交给操作系统,操作系统执行完成之后将执行结果返还给我们的应用进程
- 几种典型的应用编程接口
- UNIX环境下 套接字接口 简称套接字 socket
- 微软的 Windows Socket Interface 即 WINSOCK
- Socket API
- 适用于绝大多数操作系统
- 实现了应用进程间通信的抽象机制
- 面向多种协议栈接口 TCP /IP
- Internet网络应用最为典型的API接口
- 通信模型:客户/服务器 (C/S)架构
问题:主机和客户端都运行了多个应用进程,如何保证客户端进程和与之对应的服务器应用进程正确匹配呢?
- IP地址只能区分 主机
- 考虑到 跨主机进程通信需要传输层的支持,使用端口号区分 不同进程之间的标识
- 服务器对外提供服务 需要提供 IP地址 + 端口号
- 但是服务器内部使用套接字描述符来管理套接字,这个描述符本质就是一个结构体指针,结构体内部详细记录了字段对应的信息
- Socket抽象,其操作类型对于文件的操作,将其看做一个特殊的文件
- 返回的套接字描述符
- 最为关键的是地址信息
- 短点地址 = IP地址 + 端口号
- 使用套接字的时候需要指定本地和远程的IP地址和端口号 ,就需要使用sockaddr_in设置端点地址,就包含了IP地址和端口号等信息
- 地址族:一般使用AF_INET 涉及到不同的协议栈
Socket API函数 (WinSock)
- 基于linux的socket进行扩充
- 前面加入了WSA 表示这个版本的 套接字工具 采用动态链接库的方式进行创建
WSAStartup
WSACleanup
- 不带WSA的函数接口适用于 WIN或者Linux,带上WSA的函数只能适用于win环境下
- int WSACleanup(void);
- 应用程序在完成对请求的Socket库的使用,最后需要调用WSACleanup函数,从而解除和Socket库的绑定,释放Socket库所占用的系统资源
socket
- 创建套接字,衔接 应用层和传输层之间的数据通信
- 套接字的类型
- 使用的是TCP类型 套接字的类型是SOCK_STREAM
- 使用的是UDP类型 套接字的类型是 SOCK_DGRAM
- 跳过传输层,直接实现 应用层和网络层之间的数据通信,使用SOCK_RAW,需要特殊的权限,linux操作的话需要具备root权限,win需要用户具备管理员权限,其具备 上述两种方式的独特之处
- TCP 和 UDP的区别
- TCP:可靠 (数据不会出错、丢失、乱序等)、面向连接、字节流传输、点对点
- UDP:不可靠、无连接、数据报传输
closesocket
- win使用的是closesocket
- linux使用的是close
- int closesocket(SOCKET sd);
- 关闭一个描述符为sd的套接字
- 如果多个进程共享一个套接字的话,调用closesocket将套接字的引用计数减一,减少至0才会真正关闭
- 一个进程中的多个线程对一个套接字的使用无计数
- 即同一个进程中的一个线程使用closesocket关闭套接字,这个进程中的其余线程也不能访问这个套接字
- 返回数值
- 0 : 成功
- SOCKET_ERROR : 失败
bind
- socket创建的时候,内部的套接字描述符并没有涵盖地址信息,比如IP地址和端口号,需要使用bind进行绑定,设定套接字的本地断点地址
- 参数
- 套接字描述符:也就是使用socket创建的
- 端点地址
- 结构 sockaddr_in
- 客户程序不需要调用bind函数,因为操作系统会帮助用户填充IP地址和端口号
- 服务器需要使用这个函数指定IP地址和端口号
- IP地址如何绑定呢?
- 如果主机安置了不同的网卡,分别连接在不同的网段,造成了网段隔离
- 所以需要使用地址通配符 INADDR_ANY
- IP地址如何绑定呢?
listen
- int listen(sd,queuesize);
- 置服务器端的流套接字处于被动监听状态
- 仅仅服务端调用
- 仅用于面向连接的流套接字
- 设置了连接请求队列的大小
- 返回数值
- 0 成功
- SOCKET_ERROR:失败
connect
- connect (sd,saddr,saddrlen)
- 客户端套接字 sd
- 特定计算机的特定端口 saddr
- 只适用于客户端,使用connect函数和服务器进行连接
- 通信协议
- TCP:建立TCP连接
- connect返回成功,表示建立连接,可以成功通信
- UDP:指定服务器的端点地址
- connect返回成功,可不一定会成功通信
- TCP:建立TCP连接
accept
- newsock = accept(sd,caddr,caddrlen);
- 服务端调用accept函数从处于监听状态的流套接字sd中的客户连接请求队列中取出排在最前面的一个客户请求,并且创建一个新的套接字 衔接 来自客户端的套接字,形成一个通道
- 注意事项:
- 仅用于TCP套接字
- 仅用于服务器
- 创建新的套接字,使用新的套接字 和 客户端 进行通信
- 原因
- TCP是面向连接的、可靠的、点对点的。
- 也就是客户端和服务端 通过一个Socket建立连接,然后创建新的socket负责 客户端和服务端的应用进程之间的通信,出于高并发的思想采用上述设计
- 要不 服务端只能为一个客户端提供服务,就不能同时为其余客户端的用户进行服务
- 主线程或主进程继续监听新的请求,子线程通过创建新的连接请求
- 原因
send / send to
- send (sd,*buf,len,flags)
- sendto(sd,*buf,len,flags,destaddr,addrlen)
- send函数TCP套接字(客户和服务器) 或调用了connnect函数的UDP客户端套接字,适用于TCP,因为已经连接了,就不需要ip地址和端口号了
- sendto函数适用于UDP服务器套接字 与 没有调用connect函数的UDP客户端套接字
recv / recv from
- recv(sd,*buffer,len,flags)
- rtecvfrom(sd,*buf,len,flags,senderaddr,saddrlen)
- recv 函数从TCP连接的另外一端接收数据,或者从调用了connect函数的UDP客户端套接字接收服务器发来的数据
- recvfrom 函数从UDP服务器套接字与未调用connnect函数的UDP的客户端套接字接收对端数据
setsockopt 和 getsockopt 使用不多
小结
- connect 如果是tcp的话是真正的连接,如果是UDP没有连接,仅仅指定一个端口和地址
网络字节顺序
- 表示层 解决数据表示转换功能
- TCP/IP 定义了标准的用于协议头中的二进制的整数表示:网络字节顺序
- 某些Socket API函数的参数需要存储为网络字节顺序 (IP地址和端口号等等)
- 可以实现本地字节顺序和网络字节顺序的转换
- htons: 将本地字节顺序 转换为 网络字节顺序 (16bits)
- ntohs: 将网络字节顺序 转换为 本地字节顺序 (16bits)
- htonl : 将本地字节顺序 转换为 网络字节顺序 (32bits)
- ntohl: 将网络字节顺序 转换为 本地字节顺序 (16bits)
解析服务器的IP地址
- 客户端使用域名、IP地址标识服务器,但是IP协议需要的是32位二进制的IP地址,因此需要将域名或IP地址转换为32位IP地址
- 函数
- inet_addr() 实现点分十进制IP地址到32位IP地址的转换
- gethostbyname()实现域名到32位IP地址的转换
- 返回一个指向hostent结构体的指针
解析服务器(熟知)端口号
- 客户端还可以使用 服务名 标识服务器端口
- 需要将服务器名 转换为 熟知的端口号
- 函数
- getservbyname()
- 返回一个指向servent结构的指针
- getservbyname()
解析协议号
- 客户端可能使用协议名 如 TCP 指定协议
- 需要将协议名转换为协议号 如 6
- 函数
- getprotobyname()实现协议名到协议号的转换
- 返回一个指向结构protoent的指针
TCP客户端软件
- 确定服务器的IP地址和端口号
- 创建套接字
- 分配本地端点地址(IP地址和端口号)
- 连接服务器(套接字)
- 遵循应用层协议进行通信
- 关闭释放连接
UDP客户端软件
- 确定服务器的IP地址和端口号
- 创建套接字
- 分配本地端点地址(IP地址和端口号)
- 指定服务器端点地址 构造UDP数据包
- 遵循应用层协议进行通信
- 关闭释放连接
客户端软件的实现 connectsock()
- 设计一个connectsock过程封装底层的代码
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<winsock.h>#ifdef INADDR_NONE
#define INADDR_NONE 0xffffffff
#endif /*INADDR_NONE*/void errexit(const char *);
/*connectsock --> allocate & connect a socket using TCP or UDP
*///host 服务器端点地址
//service 服务名
//transport 传输层协议
SOCKET connectsock(const char * host,const char* service,const char * transport){struct hostent *phe; //pointer to host information entry struct servent *pse; //pointer to service information entrystruct protoent *ppe; //pointer to protocal iunformation entrystruct sockaddr_in sin; //an Internet endpoint addressint s,type; //socket descriptor and socket typememset(&sin,0,sizeof(sin));sin.sin_family = AF_INET;//Map service name to port numberif(pse = getservbyname(service,transport)){sin.sin_port = pse.s_port;}else if((sin.sin_port = htons((u_short)atoi(service))) == 0){errexit("can't get \"%s\" service entry\n",service);}//Map host name to IP address,allowing for dotted decimal_pointif(phe = gethostbyname(host)){memcpy(&cin.sin_addr,phe->h_addr,phe->h_length);}else if((sin.sin_addr.s_addr = inet_addr(host)) == INADDR_NONE){errexit("can't get \"%s\" host entry\n",host);}//Map protocol name to protocal numberif(ppe = getprotobyname(transport) == 0){errexit("can't get \"%s\" protocal entry\n",transport);}//Use protocal to choose a socket typeif(strcmp(transport,"udp")==0){type = SOCK_DGRAM;}else{type = SOCK_STREAM;}//Allocate a sockets = socket(PF_INET,type,ppe->p_proto);if(s == INVALID_SOCKET){errexit("can't create socket:%d",GetLastError());}//Connect the socketif(connect(s,(struct sockaddr*)&sin,sizeof(sin)) == SOCKET_ERROR){errexit("can't connect to %s.%s: %d\n",host,service,GetLastError());}return s;
}
- UDP客户端 和 TCP客户端
#include <winsock.h>
SOCKET connectsock(const char*,const char*,const char*);//connectUDP -> connect to a specified UDP service on a specified host
SOCKET connectUDP(const char*host,const char* service){return connectsock(host,service,"udp")
}#include <winsock.h>
SOCKET connectsock(const char*,const char*,const char*);//connectUDP -> connect to a specified UDP service on a specified host
SOCKET connectTCP(const char*host,const char* service){return connectsock(host,service,"tcp")
}
- 异常处理
#include<stdarg.h>
#include<stdio.h>
#include<stdlib.h>
#include<winsock.h>//errexit -> print an error message and exitvoid errexit(const char* format){va_list args;va_start(args,format);vfprintf(stderr,format,args);va_end(args);WSACleanup();exit(1);
}
访问DAYTIME服务的客户端
- 获取日期和时间
- 双协议服务(TCP\UDP)
- 端口号为 13
- localhost 说明 服务端和客户端都部署在同一台机器上
- daytime 说明这是一个请求时间的服务
TCP
- 代码采用循环接收信息的方式来接收,是因为tcp是一种流传输的协议,发送端发送的数据并不意味着接收端收到的数据一样 ,有可能数据切片
UDP
UDP使用数据报的方式,因此每次发送和接收的数据是完整的,因此不需要使用循环,只需要一次接收即可
四种类型基本服务器
-
循环无连接 UDP
- 流程
- 创建套接字
- 绑定端点地址 INADDR_ANY + 端口号
- 反复接收来自客户端的请求
- 遵循应用层协议,构造响应报文,发送给客户
- 数据发送
- 服务器不可以使用connect()函数
- 无连接服务器使用sendto()函数发送数据报
- 数据接收
- 使用recvfrom函数接收数据,自动提取
- 流程
-
循环面向连接 TCP
- 创建(主)套接字 并绑定端口号
- 设置(主)套接字为被动监听模式,准备用于服务器
- 调用accept()函数接收下一个请求(通过主套接字),创建新的套接字用于和客户端建立连接
- 遵循应用层协议,反复接收客户请求,通过新的套接字构造并发送响应报文,发送给客户
- 完成为特定的客户端服务之后,关闭和客户端之间的连接 返回步骤3
-
并发无连接
- 主线程 第一步 创建套接字 并绑定端口号
- 主线程 第二步 反复调用recvfrom()函数 接收下一个客户端的请求,并创建新线程 处理客户响应
- 子线程 第一步 接受特定的请求
- 子线程 第二步 依据应用层的协议构造响应的报文 并调用sendto()发送
- 子线程 第三步 退出(子线程处理完成一个请求之后便会终止)
- 注意事项:
- 主线程仍然在调用 recvfrom函数,不断的接收请求 创建线程响应服务
- 并发面向连接
- 主线程 第一步 创建(主)套接字 并绑定端口号
- 主线程 第二步 设置(主)套接字为被动监听模式,准备用于服务器。
- 主线程 第三部 反复调用accept()函数 创建主套接字,接收下一个客户端的下一个连接请求,并创建新线程 处理客户响应
- 子线程 第一步 接受特定的请求
- 子线程 第二步 依据应用层的协议与特定的用户进行交互
- 子线程 第三步 退出(子线程处理完成一个请求之后便会终止)(线程终止)
代码