文章目录
在目前的网络体系中,TCP/IP是主流,也是应用最广泛的,因此,本课程仅讨论TCP/IP下面的网络编程技术。
一.常用的端口号及其服务
端口号 | 服务 |
---|---|
21 | ftp文件传输 |
22 | ssh远程连接 |
23 | telent终端仿真 |
25 | smtp服务 |
53 | DNS域名解析 |
80 | http超文本传输 |
443 | https加密的超文本传输服务 |
3306 | mysql服务 |
6379 | redis服务 |
8080 | tcp服务默认段端口号 |
8888 | Nginx服务 |
27017 | mongoDB服务 |
二.套接字的分类及说明
1.流式套接字
提供双向(全双工)、可靠、有序的数据传输,但传输前需要建立连接,成本较高,适合进行大数据量连续传输,只能进行一对一通信。类似于打电话。
2.数据报套接字
提供不可靠、不保证顺序的数据传输,但不需要建立连接,成本较低,适合于突发的、高频的、少量的数据传输,可以进行一对一通信和一对多通信。类似于收发短信。
三.涉及到的函数
返回值一般是成功返回0,失败返回-1
1.查询函数的使用说明
man 2 函数名称
例如查询socket : man 2 socket
2.创建套接字
函数:
int socket(int domain, int type, int protocol)
含义:
相当于安装电话
参数:
名称 | 含义 | 值 |
---|---|---|
domain | 指定套接字的作用域(同一台计算机还是要和别的计算机通信) | AF_INET:Address Family指定TCP/IP协议家族;PF_INET:Protocol ;Family,AF_UNIX:用于同一台计算机的进程间通信;AF_INET6:ipv6网络协议 |
type | 套接字的类型 | SOCK_STREAM:流套接字,对应TCP协议;SOCK_DGRAM:数据报套接字,对应UDP协议;SOCK_RAW:原始套接字,提供原始网络协议存取 |
protocol | 协议类型 | 传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP; 网络层:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL);这个一般写0就可以了 |
返回值:
成功:返回描述符
失败:返回-1
3.关闭套接字
函数:
int close(int fd)
含义:
挂掉电话
参数:
名称 | 含义 | 值 |
---|---|---|
fd | 套接字的描述符 | 整数 |
返回值:
成功则返回0,错误返回-1
4.服务端绑定地址信息
函数:
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen)
含义:
给你家电话一个电话号码,让别可以找到你家
参数:
名称 | 含义 | 值 |
---|---|---|
sockfd | 套接字描述符 | 整数 |
myaddr | 地址和端口号 | int main(int argc, char **argv) struct sockaddr_in addr1, addr2; //fill server address bzero(&addr1, sizeof(addr1)); addr1.sin_family = AF_INET; addr1.sin_port = htons(atoi(argv[1])); |
addrlen | 地址的长度 | sizeof(myaddr) |
返回值:
成功返回0,失败返回-1
5.其他的一些辅助函数
**1.int inet_aton(const char ipstring, struct in_addr inp)
将点分十进制形式的IP地址字符串转换并填充到in_addr类型的结构体中,若ipstring为一个合法的点分十进制地址,返回值为非0,否则为0。该函数较inet_addr更健壮。
*2.in_addr_t inet_addr(const char ipstring)
将点分进制的IP地址字符串转换为32位、网络字节顺序的整数
若ipstring是一个合法的IPv4地址,会返回32位整数,否则会返回INADDR_NONE。
*3.char inet_ntoa(struct in_addr addr)
将in_addr类型的描述IP地址信息的结构体转换为x.x.x.x形式的字符 串
4.字节顺序转换
uint32_t htonl(uint32_t hostlong)
uint16_t htons(uint16_t hostshort)
uint32_t ntohl(uint32_t netlong)
uint16_t ntohs(uint16_t netshort)
h - host/local, n - network, l - long, s - short
有些函数或结构体要求字节顺序是本机字节顺序,有些要求网络字节顺序
字节顺序很重要,若不谨慎处理,会导致发送方和接收方理解上的偏差,从而导致通信错误。
6.通用的函数
1.获取套接字信息
int getpeername(int sockfd, struct sockaddr*addr, socklen_t *addrlen)
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
前者获取与sockfd对应的对端地址信息,后者获取sockfd使用的本地地址信息
成功返回0,失败返回-1。
2.获取主机名称
int gethostname(char *name, size_t len)
成功返回0,失败返回-1。若主机名长度超过len,会被截断。获取的名称可能是一个简单的名字,也可能是域名。
3.获取主机信息
struct hostent *gethostbyname(const char *name)
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type)
成功返回有效指针,否则返回NULL。返回的指针是栈空间,其内容可能会被后续的调用覆盖,因此,最好将结果深度复制后长期保存。
4.获取协议信息
struct protoent *getprotobyname(const char *name)
struct protoent *getprotobynumber(int proto)
成功返回有效指针,否则返回NULL。返回协议信息,该信息是IPv4报文中的协议字段的内容。
5.获取服务信息
struct servent *getservbyname(const char *name, const char *proto)
struct servent *getservbyport(int port, const char *proto)
成功返回有效指针,否则返回NULL。返回服务信息。若proto为NULL,则会匹配任意协议。
6.套接字选项读写
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t optlen)
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen)
level指选项级别,包括SOL_SOCKET,IPPROTO_IP,IPPROTO_TCP,IPPROTO_UDP,IPPROTO_ICMP等,前者是设置套接字选项,后面几个用于直接构造报文,对应的套接字类型为SOCK_RAW。
optname为选项名称,如对于SOL_SOCKET,有SO_BROADCAST,SO_REUSEADDR、SO_DONTROUTE、SO_KEEPALIVE等选项,对于IPPROTO_IP,有IP_ADD_MEMBERSHIP、IP_DROP_MEMBERSHIP、IP_MULTICAST_IF、IP_MULTICAST_LOOP、IP_MULTICAST_TTL、IP_OPTIONS等,对于其它level也有不同的选项。
optval是选项值,选项一般为整型数,也可能为结构体类型。
7.监听函数
函数:
int listen(int sockfd, int backlog)
含义:
用于监听连接请求并处理,而不用于数据的发送和接收处理,因此称为监听套接字。为了管理众多的连接请求,协议栈会为监听套接字创建一个队列,用于容纳待处理的连接请求。
8.接收客户端的请求
函数:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
含义:
接收客户端的请求
返回值:
操作成功,会返回响应套接字描述符。
9.连接服务端
函数:
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen)
含义:
连接到服务端
返回值:
如果连接请求未能进入对方监听套接字的连接请求队列,会返回-1,errno是ECONNREFUSED或其它值,应根据其值进行处理。
9.发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags)
操作成功,返回发送的字节数。write()等价于无flag参数的send()。
10.接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags)
返回值大于0,表示接收的字节数;返回值为0,表示对方关闭了数据发送通道(即半关闭)。read()等价于无flag参数的recv()。
四.涉及到的头文件
名称 | 含义 |
---|---|
sys/socket.h | 主文件 |
sys/types.h | 数据类型的声明 |
sys/select.h | select I/O模型 |
sys/epoll.h | epoll I/O模型 |
netinet/in.h | Internet地址 |
arpa/inet.h | Internet地址转换 |
netdb.h | 网络数据库相关,例如域名解析 |
errno.h | ISO C99标准错误代码,仅定义一个全局变量errno |
unistd.h | 标准符号常量和类型 |
fcntl.h | 文件控制 |
五.linux多线程
1.进程的概念
**组成:**代码、数据和进程控制块(PCB,Process Control Block)组成,PCB是进程管理的核心
解释:
每个进程都有一个唯一标识符PID,它是对进程进行控制的依据
进程是资源分配和CPU调度的基本单位
一个程序可以对于多个进程例如,在Windows系统中同时编程多个Word文档,这时,每个Word文档的编辑、处理过程都是一个进程,但这些进程的代码却属于同一个程序。
2.线程的概念
1、线程thread用于描述代码的微观的运行情况,是CPU调度的基本单位。
2、线程是依附于进程的,每个进程至少包含一个主线程,线程运行过程中,可以根据需要创建新的线程,指派新线程要执行的代码(函数或方法)。
3、若进程执行期间同时存在多个线程,这些线程之间可能会因为共享资源导致运行错误,因此,需要在线程代码中对共享资源进行同步/互斥处理(多线程技术的重点和难点)。当进程的主线程执行完毕后,其它线程也会被关闭。
3.进程通信
1、大部分OS都支持多进程技术,即有多个进程 “同时”处于运行状态。
2、部分情况下,进行之间可能不需要进行数据交换,有些情况下,需要进行数据交换,目的是合作完成某项任务。进程之间的数据交换可称为进程通信,通过双方是主机同的两个进程。
3、进程的通信方式包括消息、邮槽、管道、共享存储区、共享文件等。
4.多线程管理
Linux支持多线程技术,需要借助pthread库,需要在源代码中#include <pthread.h>。使用gcc/g++编译时,需要指定连接选项-lpthread。
1.创建线程
int pthread_create(pthread_t *restrict tid, const pthread_attr_t *restrict attr, void *(*start_routine)(void *), void *restrict arg)
restrict用于修饰指针,在此可以忽略。如果对线程属性采用默认设置,attr可以为NULL;参数3为线程函数指针,线程函数原型必须为void *xxx(void *);参数4为传递给线程函数的参数
2.等待线程结束
int pthread_join(pthread_t tid, void **status)
等待指定线程结束,在线程协作中会用到。若需要用到线程的执行结果,就使用join方式,否则使用detach方式。
int pthread_detach(pthread_t tid)
指定线程结束后,其资源会被系统自动释放。与pthread_join类似,但当前函数不会等待线程结束就会继续执行。
3.结束当线程
void pthread_exit(void *retval)
六.套接字函数的阻塞与非阻塞
相对于数据报UDP套接字,基于连接的流式TCP套接字更加容易出现阻塞现象。
由于accept()可能会阻塞,因此会导致以下现象:
1.当监听队列为空时,会一直等待,直到新的连接请求到来或出现错误。
2.recv()/recvfrom():当没有数据到达时,会一直等待,直到数据到达或出现错误。
3.connect():当客户端的连接请求到达服务端,全服务端未及时处理时,会一直等待。
4.send()/sendto():当套接字的发送缓冲区没有足够空间容纳待发送数据时,会一直等待。理论上会出现这种现象,但系统资源较充足时比较少见。
1.设置套接字为非阻塞模式
1.在创建套接字时设置
socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0)
上面代码创建的套接字工作在非阻塞模式,若不指定SOCK_NONBLOCK特征则会工作在阻塞模式。
2.单独调用fcntl函数进行设置
fcntl()是一个对文件描述符进行设置的函数,对套接字同样有效。命令为F_SETFL,值为O_NONBLOCK,一般用如下方式处理,或运算的目的是在保留描述符原有操作特性的基础上,增加非阻塞模式的设置。
fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) | O_NONBLOCK);
如果需要在套接字使用过程中修改阻塞或非阻塞模式,适合使用第2种方式。
七.windows平台的消息处理和执行环境
1.消息处理
消息处理函数
a)回调函数,有规定的原型和处理规则
b)普通窗口和对话框的消息处理函数稍有不同
LRESULT CALLBACK DialogProc(HWND hDlg,UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg)
{
case WM_INITDIALOG:
进行对话框初始化处理,例如设置原始的或默认的输入、状态等
return TRUE;
case WM_COMMAND:
处理产生在各种组件上的事件
if(LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
默认处理:遇到回车或ESC键或关闭按钮,一般是退出对话框处理,对话框会消失
可以根据情况自行决定如何处理
EndDialog(hDlg, LOWORD(wParam);
return TRUE;
}
return FALSE;
}
强调:
1)处理了某个消息,返回TRUE;否则返回FALSE。不要画蛇添足!
2)一定要在某处调用EndDialog()!
2.执行环境
1)需要头文件winsock2.h,编译时需要ws2_32.lib。
2)使用WinSock函数前需要调用WSAStartup()准备环境
3)WinSock使用完毕后,调用WSACleanup()处理善后工作
八.IO模型(这里会出两个简单题)
第一个简答题盲猜是:简述套接字使用框架?
答案:
1.创建一个套接字
2.若是被动等待,将套接字s绑定到某个本地地址
3.按照通过规程发送或接收数据
4.通信完成,关闭套接字
1.select模型
1、前面流式套接字并发服务端的问题
1)非阻塞模式:编程比阻塞模式相对繁琐,需要进行大量操作条件不具备而出现的EAGAIN或EWOULDBLOCK错误
2)多线程技术:线程有一定的开销,尤其是连接数量很大时
对于流式套接字,服务端
a)串行:原因是套接字的工作模式,单线程,tcps1.cpp
b)并发:将套接字工作模式设置为非阻塞,单线程,tcps2.cpp
c)并发:多线程技术,套接字可以工作在阻塞或非阻塞模式,tcps3.cpp
2、I/O复用模型
1)每种OS都提供输入/输出模型,可以同时管理多个套接字,仅使用一个线程就可以同时为多个客户端服务,避免了非阻塞模式的繁琐处理过程,同时也避免了阻塞现象的出现。
2)Linux:select,poll(不讲),epoll
3)Windows:select(与Liunx的细节有些区别),WSAAsyncSelect,WSAEventSelect(不讲),IOCP(不讲)
3、select模型
1)该关注的套接字(描述符)存储在fd_set集合中,select模型会监控一段时间内集合中所有有效的套接字是否“操作就绪”,若是会保留下来,否则会将该套接字移出集合,对于仍然存在于集合中的套接字,可以直接进行相应的操作,而不必再花时间等待。
2)对套接字的关注只有三种类型:数据是否可读?数据是否可写?是否发生错误?为方便,模型按感兴趣的类型将套接字集合分为三种,某个套接字可以只加入一个集合,也可以同时加入多个集合。
3)select函数
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
a)nfds:三个集合中所包含的最大描述符值+1(每个描述符是一个整数)
b)readfds:若关注套接字是否可读(对响应套接字,有数据到达,可以成功调用recv;对监听套接字,有连接请求到达,可以成功调用accept),应将其加入该集合,一般来说,所有套接字均应加入该集合
c)writefds:若关注套接字是否可写,应将其加入该集合。“可写”仅对响应套接字有效,表示能够成功调用 send()(本质是套接字的发送缓冲区有空间)
d)exceptfds:异常套接字集合,若希望监控某套接字出现异常状态,应加入该集合。
e)timeout:超时时间设置,其类型是:
struct timeval {
long tv_sec; //seconds
long tv_usec; //microseconds
};
若timeout的两个成员均为0,则select()会立即返回;若timeout设置为NULL,则会无限期等待,直到出错或有描述符期待的事件发生
timeout设置的值不应太大或太小
f)返回值:
f.1)-1:执行过程中出现错误,代码保存在errno中,出错时集合不会被修改
f.2)0:在规定时间段内所有集合中的描述符均未变为就绪状态,即感兴趣事件未发生
f.3)>0:三个集合中处于就绪状态的描述符总数,在这种情况下才有必要进行后续的处理
g)select()会修改timeout的内容,因此,在每次调用前都要设置适当的时间间隔
h)该模型并不仅仅用于套接字,也适用于文件
4、select模型的操作宏
1)FD_ZERO(fd_set *):清空某个集合,仅进行一次
2)FD_SET(int, fd_set*):将某个描述符加入指定的集合
3)FD_ISSET(int, fd_set*):调用select()后,判断某套接字是否仍存在于集合中,若存在,则可以对其进行某种I/O或出错处理。注意:未处于就绪状态的描述符在select()后会被移出集合
4)FD_CLR(int, fd_set*):将某个描述符从集合中移出,即不再关注该描述符
5、流式套接字并发服务端使用select模型的框架
FD_ZERO(…)
FD_SET(…) //将监听套接字存入读集合
while(…)
{
准备集合(循环)
设置timeout
select(…)
循环:若成功,逐一判断某套接字是否仍在某集合中,若是,执行相应的I/O操作或出错处理
}
6、Windows Socket下select模型
WinSock同样支持select模型,但与Linux下的相比,有以下区别
1)fd_set结构体组成不同
2)FD_ISSET使用方式有所不同
总结:
盲猜会考这个:
流式套接字并发服务端使用select模型的框架?
答案:
FD_ZERO(…)
FD_SET(…) //将监听套接字存入读集合
while(…)
{
准备集合(循环)
设置timeout
select(…)
循环:若成功,逐一判断某套接字是否仍在某集合中,若是,执行相应的I/O操作或出错处理
}
2.epoll模型
1、select模型缺陷——性能低
每次调用select()前,都需要准备描述符集合,调用完成后,还需要逐一检测集合中每个描述符上是否有期待事件产生,性能低下的原因是频繁从用户态的应用程序向系统态的核心提交需要监控的描述符,系统开销较大。如果不用频繁向内核重复提交要监控的描述符,则性能会大幅提升。
2、epoll模型
1)epoll是Linux上的高性能I/O模型。Kqueue是FreeBSD系统中的模型,Windows是IOCP模型。
2)下图中体现select与epoll模型性能的差异,很明显,当连接数量较大时,select模型的性能明显下降,epoll模型的性能比较稳定,不会随着管理描述数量的增加出现明显性能下降的情况。
3)epoll适用于连接数量较多,但活动连接数量相对较少的情况。
3、epoll模型用法
1)使用框架(仅针对流式套接字)
epoll_create()
创建epoll_event存储空间,以便应用程序和OS内核使用
监听套接字交由epoll管理:epoll_ctl(…)
while
{
fdc = epoll_wait()
if(fdc > 0)
{
if(…) epoll_ctl(…)
对于监听套接字和响应套接字分别处理(类似于select()调用以后的处理)
}
}
2)创建epoll实例
int epoll_create(int size)
epoll模型由内核管理,需要向内核申请创建实例。参数size为实例大小,可以理解为所管理描述符的数量。在Linux 2.6.8以后,size会由系统自动调整,该参数仅仅为保持兼容而存在。
若操作成功,会返回引用该实例的描述符,否则返回-1。
3)注册描述符及事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epoll模型将管理若干描述符及关心的事件(与select作用类似),这些管理功能均通过该函数表示。操作成功,返回0,否则返回-1。各参数含义:
a)epfd为epoll实例描述符
b)fd为要管理的描述符
c)event为该描述符上关心的事件,类型为结构体类型:
struct epoll_event{
__uint32_t events;
epoll_data_t data;
};
events是由以下事件标志通过 | 运算得到的组合
EPOLLIN:可读数据(有数据到达)
EPOLLOUT:可写数据(可发送给对方)
EPOLLPRI:对于流式套接字,收到了OOB数据
EPOLLRDHUP:对方调用了close()或shutdown(),在ET模式下很有用
EPOLLERR:发生错误的情况
EPOLLET:启用ET(Edge Triggered,边缘触发,状态发生变化)模式,默认模式为LT(Level Triggered,条件或水平触发,状态不变,但总满足“操作就绪”的要求)
EPOLLONESHOT:只使用一次,单独使用无意义
除上述标志以外,Linux的不同版本还增加了其它事件标志。
data成员表示事件对应的数据,其定义为:
typedef union epoll_data{
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
}epoll_data_t;
d)op表示要执行的管理操作,有三个:EPOLL_CTL_XXX,其中XXX是ADD、DEL、MOD三者之一,分别表示新注册、删除已有、修改已有
4)进行监控
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
执行完成后,若有描述符上期待的事件发生,返回描述数个数;若超时,返回0;若出错,返回-1。参数含义为:
a)epfd,epoll实例的描述符
b)events,用户动态申请的内存空间,用于存放产生事件的描述符等信息,若有n个描述符上产生了期待的事件,这n个描述符的相关信息存放在events的前n个元素中
c)maxevents,events空间大小
d)timeout,该参数与select模型中的不同,它以毫秒为单位设置时间间隔。若为0,调用会立即返回;若为-1,则无限期等待,直至出错或有描述符发生事件;若为其它值,则会等待指定的时间间隔,在此时间内,若有事件发生,会返回,否则会等到超时再返回。该参数设置会影响性能
4、epoll模型的应用*
很多高性能的Linux服务软件中均会采用,例如Ngnix的Linux版本。自行学习
总结:
盲猜会考这个:
epoll模型用法?
答案:
epoll_create()
创建epoll_event存储空间,以便应用程序和OS内核使用
监听套接字交由epoll管理:epoll_ctl(…)
while
{
fdc = epoll_wait()
if(fdc > 0)
{
if(…) epoll_ctl(…)
对于监听套接字和响应套接字分别处理(类似于select()调用以后的处理)
}
}
九.基于tcp的串行服务端
基于tcp的串行服务端,通信规程和课堂讲的非常类似,有整数,有字符,Linux平台
这个不会…