文章目录
- 1.套接字概念
- 2. 网络字节序
- 3.IP地址转换函数
- 4.sockaddr数据结构
- 5.网络套接字函数
- 6.TCP服务器函数
- 7.高并发服务器
- 8.本章扩展
现在我们学习一种不同的主机间进程通信方法: socket
本文福利, 免费领取Linux C/C++全栈开发学习资料包、技术视频/代码,1000道大厂面试题,内容包括(C++基础,网络编程,数据库,中间件,后端开发/音视频开发/Qt开发/游戏开发/Linuxn内核等进阶学习资料和最佳学习路线图)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓
1.套接字概念
Socket本身有“插座”的意思,成对出现,在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
套接字的内核实现较为复杂,不宜在学习初期深入学习。
在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。
套接字通信原理如下图所示:
**在网络通信中,套接字一定是成对出现的。**一端的发送缓冲区对应对端的接收缓冲区。我们使用同一个文件描述符索发送缓冲区和接收缓冲区
2. 网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。(大端小端在文章下方扩展)
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如网络基础的的UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); //32位无符号整型的主机字节顺序到网络字节顺序的转换(小端->>大端)
uint16_t htons(uint16_t hostshort);//16位无符号短整型的主机字节顺序到网络字节顺序的转换 (小端->>大端)
uint32_t ntohl(uint32_t netlong); //32位无符号整型的网络字节顺序到主机字节顺序的转换 (大端->>小端)
uint16_t ntohs(uint16_t netshort); //16位无符号短整型的网络字节顺序到主机字节顺序的转换 (大端->>小端)
h表示host,n表示network,l表示32位长整数,s表示16位短整数。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
示例:
3.IP地址转换函数
3.1inet_pton函数
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
功能: 将点分十进制串 转成32位网络大端的数据("192.168.1.2" ==>192.168.1.2 )
参数:
af :
AF_INET IPV4
AF_INET6 IPV6
src: 点分十进制串的首地址
或者使用宏:
INADDR_ANY 通配地址,值为0
dst : 32位网络数据的地址 -->(void*)&dst
成功返回1
失败返回-1
3.2 inet_ntop函数
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
功能: 将32位大端的网络数据转成点分十进制串
参数:
af : AF_INET
src : 32位大端的网络数 地址
dst : 存储点分十进制串 地址
size : 存储点分制串数组的大小
例:“192.168.1.1” -》255.255.255.255 15+'\0'最低16个
或者使用宏:
INET_ADDRSTRLEN 宏的值 16
INADDR_ANY 通配地址,值为0
返回值: 存储点分制串数组首地址
代码示例:
#include<stdio.h>
#include<arpa/inet.h>
int main()
{
char buf[]="192.168.1.4";
unsigned int num = 0;
inet_pton(AF_INET,buf,&num);//一般用在发送转网络
unsigned char* p = (unsigned char*)#
printf("%d %d %d %d\n",*p,*(p+1),*(p+2),*(p+3));
char ip[16] = " ";
printf("%s\n",inet_ntop(AF_INET,&num,ip,16));//用于接收打印
return 0;
}
运行结果:
xcc@ubuntu:~/test $ ./a.out
192 168 1 4
192.168.1.4
4.sockaddr数据结构
网络通信要解决三大问题:协议,IP,端口,我们把它们放到一个结构体里,假如我要发送数据我需要数据内容,ip,端口;方便点用结构体封装起来
strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
可参看 man 7 ip
ipv4套接字结构体
struct sockaddr_in {
sa_family_t sin_family; //协议 AF_INET
in_port_t sin_port; //端口
struct in_addr sin_addr; //ip地址
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */ 4字节
};
ipv6套接字结构体
struct sockaddr_in6 {
unsigned short int sin6_family; /* AF_INET6 */
__be16 sin6_port; /* Transport layer port # */
__be32 sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
__u32 sin6_scope_id; /* scope id (new in RFC2553) */
};
这里会产生一个矛盾:
我这里有ipv4和ipv6,到底是用ipv4 还是ipv6呢?总不能各定义一个接口,分别处理它们吧;那要是有一个int类型和char类型,我们怎么直接使用它们呢?可以用万能类型:void* 成为通用结构体,不管是什么协议类型都可以调用
当然这里没有void,我们可以把struct sockaddr*看成void*,目的为了接口统一
通用套接字结构体
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
它们之间的协议都一样,不管ipv4还是ipv6,取它们的地址都能找到
Pv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址,IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128位IP地址和一些控制字段。UNIX Domain Socket的地址格式定义在sys/un.h中,用sock-addr_un结构体表示。各种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度(并不是所有UNIX的实现都有长度字段,如Linux就没有),后16位表示地址类型。IPv4、IPv6和Unix Domain Socket的地址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下,例如:
struct sockaddr_in servaddr;
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)); /* initialize servaddr */
5.网络套接字函数
5.1 socket模型创建流程图
使用NetAssist.exe工具实现指令通信
5.2 socket函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:创建套接字
domain:
AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
AF_INET6 与上面类似,不过是来用IPv6的地址
AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
type:
SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
SOCK_SEQPACKET该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序
protocol:
传0 表示使用默认协议。
返回值:
成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,domain参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可。
tcp传输控制协议特点: 出错重传 每次发送数据对方都会回ACK, 可靠 tcp是抽象打电话的模型建立练级 使用连接 关闭连接
5.3 connect函数
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockdf:
socket文件描述符
addr:
传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen:
传入参数,传入sizeof(addr)大小
返回值:
成功返回0,失败返回-1,设置errno
客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。
5.4 TCP-client:
include <unistd.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define SIZE 1024
int main()
{
//创建套接字
int sock_fd = -1;
char buf[SIZE]=" ";
struct sockaddr_in addr;
sock_fd = socket(AF_INET,SOCK_STREAM,0);
if( -1 == sock_fd)
{
perror("socket");
return 1;
}
//连接服务器
addr.sin_family = AF_INET; //ipv4
addr.sin_port = htons(8888);//端口号转网络字节序
inet_pton(AF_INET,"192.168.0.119",&addr.sin_addr.s_addr);//ip字串
connect(sock_fd,(struct sockaddr *)&addr,sizeof(addr));
//读写数据
while(1)
{
//从终端读
int n = read(STDIN_FILENO,buf,sizeof(buf));//fgets()
write(sock_fd,buf,n);//发送数据给服务器
//从服务器读取并返回个数,标准输出到屏幕
n = read(sock_fd,buf,sizeof(buf));
write(STDOUT_FILENO,buf,n);
printf("\n");
}
//关闭套接字文件描述符
close(sock_fd);
return 0;
}
首先打开我的NetAssist.exe,代码是连接服务器操作,在协议类型中选择TCP Server,然后查看本地IP地址,并设置端口号,随意设置,代码内容也需要同步ip和端口号(需联网)下载地址
效果展示:
6.TCP服务器函数
6.1 TCP服务器通信流程
6.2 bind函数
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:给套接字绑定固定的端口和ip,可以netstat查看链接情况
sockfd: 套接字
addr: ipv4套接字结构体地址
addrlen: ipv4套接字结构体的大小
返回值:
成功返回0 失败返回;-1
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。
bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。如:struct sockaddr_in addr;bind(lfd,(struct sockaddr *)&addr,sizeof(addr));
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);
首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为6666。
6.3 listen函数
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能:监听套接字
参数:
sockfd : 套接字
backlog : 已完成连接队列和未完成连接队里数之和的最大值 128
超过就提取不了新的连接,被忽略
返回值:
成功返回:0 失败返回:-1
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。
6.4 accept函数
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf:
socket文件描述符
addr:
传出参数,返回链接客户端地址信息,含IP地址和端口号
addrlen:
传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
返回值:
成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno
三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址。
我们的服务器程序结构是这样的:
struct sockaddr_in cliaddr;
while (1) {
socklen_t len = sizeof(cliaddr);
cfd = accept(listenfd, (struct sockaddr *)&cliaddr, &len);
n = read(cfd, buf, MAXLINE);
......
close(cfd);
}
整个是一个while死循环,每次循环处理一个客户端连接。由于cliaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。accept()的参数listenfd是先前的监听文件描述符,而accept()的返回值是另外一个文件描述符cfd,之后与客户端之间就通过这个cfd通讯,最后关闭cfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept的参数。accept()成功返回一个文件描述符,出错返回-1。
6.5.C/S模型-TCP
下图是基于TCP协议的客户端/服务器程序的一般流程:
服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。
数据传输的过程:
建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。
如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。
在学习socket API时要注意应用程序和TCP协议层是如何交互的: 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段
6.6 TCP-server
下面通过最简单的服务器程序的实例来学习socket API
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#define SIZE 1024
//服务器通信
int main()
{
int lfd ;
int ret = -1;
struct sockaddr_in addr;
char buf[SIZE]="";
//创建套接字
lfd = socket(AF_INET,SOCK_STREAM,0);
//连接服务器
addr.sin_family = AF_INET;
addr.sin_port = htons(8000);
//addr.siz_addr.addr = INADDR_ANY;//绑定的是通配地址
inet_pton(AF_INET,"192.168.220.132",&addr.sin_addr.s_addr);//服务器的ip
//绑定
ret = bind(lfd,(struct sockaddr *)&addr,sizeof(addr));
if(ret < 0)
{
perror("");
exit(0);
}
//监听
listen(lfd,128);
//提取新连接
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&len);
char ip[16]="";
printf("new client ip=%s port=%d\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,
ip,16), ntohs(cliaddr.sin_port));//显示连接的ip地址和端口号信息
//读写
while(1)
{
bzero(buf,sizeof(buf));//等价于memset(buf,0,sizeof(buf));
//fget(buf,sizeof(buf),stdin)
int n = read(STDIN_FILENO,buf,sizeof(buf));//从屏幕输入读取到缓冲区
write(cfd,buf,n);//发送给服务器
}
//关闭
close(lfd);
close(cfd);
return 0;
}
由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。
客户端和服务器启动后可以使用netstat命令查看链接情况:
netstat -apn|grep 端口号
出现下图这种是因为服务器的端口需要过两分钟释放,后面tcp状态转换会再讲,解决方法就是换个端口(端口复用)
6.7出错处理封装函数
上面的例子不仅功能简单,而且简单到几乎没有什么错误处理,我们知道,系统调用不能保证每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以迅速得到故障信息。
为使错误处理的代码不影响主程序的可读性,我们把与socket相关的一些系统函数加上错误处理代码包装成新的函数,做成一个模块:
warp.h
#ifndef __WRAP_H_
#define __WRAP_H_
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
int tcp4bind(short port,const char *IP);
#endif
warp.c
void perr_exit(const char *s)
{
perror(s);
exit(-1);
}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
int n;
again:
if ((n = accept(fd, sa, salenptr)) < 0) {
if ((errno == ECONNABORTED) || (errno == EINTR))//如果是被信号中断和软件层次中断,不能退出
goto again;
else
perr_exit("accept error");
}
return n;
}
int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
if ((n = bind(fd, sa, salen)) < 0)
perr_exit("bind error");
return n;
}
int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
if ((n = connect(fd, sa, salen)) < 0)
perr_exit("connect error");
return n;
}
int Listen(int fd, int backlog)
{
int n;
if ((n = listen(fd, backlog)) < 0)
perr_exit("listen error");
return n;
}
int Socket(int family, int type, int protocol)
{
int n;
if ((n = socket(family, type, protocol)) < 0)
perr_exit("socket error");
return n;
}
ssize_t Read(int fd, void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = read(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)//如果是被信号中断,不应该退出
goto again;
else
return -1;
}
return n;
}
ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = write(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
int Close(int fd)
{
int n;
if ((n = close(fd)) == -1)
perr_exit("close error");
return n;
}
/*参三: 应该读取固定的字节数数据*/
ssize_t Readn(int fd, void *vptr, size_t n)
{
size_t nleft; //usigned int 剩余未读取的字节数
ssize_t nread; //int 实际读到的字节数
char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ((nread = read(fd, ptr, nleft)) < 0) {
if (errno == EINTR)
nread = 0;
else
return -1;
} else if (nread == 0)
break;
nleft -= nread;
ptr += nread;//更新位置
}
return n - nleft;
}
/*:固定的字节数数据*/
ssize_t Writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
if (nwritten < 0 && errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;
}
static ssize_t my_read(int fd, char *ptr)
{
static int read_cnt;
static char *read_ptr;
static char read_buf[100];
if (read_cnt <= 0) {
again:
if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
if (errno == EINTR)
goto again;
return -1;
} else if (read_cnt == 0)
return 0;
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return 1;
}
ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = my_read(fd, &c)) == 1) {
*ptr++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
*ptr = 0;
return n - 1;
} else
return -1;
}
*ptr = 0;
return n;
}
int tcp4bind(short port,const char *IP)
{
struct sockaddr_in serv_addr;
int lfd = Socket(AF_INET,SOCK_STREAM,0);
bzero(&serv_addr,sizeof(serv_addr));
if(IP == NULL){
//如果这样使用 0.0.0.0,任意ip将可以连接
serv_addr.sin_addr.s_addr = INADDR_ANY;
}else{
if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0){
perror(IP);//转换失败
exit(1);
}
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(port);
// int opt = 1;
//setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
Bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
return lfd;
}
7.高并发服务器
7.1多进程并发服务器流程图
一个进程一个线程是无法实现多个客服端进行处理的,想并发处理需要是多线程或多进程,下图为多进程流程图:
流程: 创建套接字-》绑定-》监听
while(1){ 提取连接
fork创建子进程
子进程中,关闭lfd,服务客户端
父进程关闭cfd,回收子进程的资源
}关闭
使用多进程并发服务器时要考虑以下几点:
父进程最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符)
系统内创建进程个数(与内存大小相关)
进程创建过多是否降低整体服务性能(进程调度)
7.1多进程并发服务器
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include "wrap.h"//自定义包裹函数
//回收子进程
void free_process(int sig)
{
pid_t pid;
while(1)
{
pid = waitpid(-1,NULL,WNOHANG);
if(pid <=0 )//小于0 子进程全部退出了 =0没有进程没有退出
{
break;
}
else
{
printf("child pid =%d\n",pid);
}
}
}
int main(int argc, char *argv[])
{
//为了不让子进程和父进程做同一样事情,先阻塞信号
//不重复打印
sigset_t set;
sigemptyset(&set);
sigaddset(&set,SIGCHLD);
sigprocmask(SIG_BLOCK,&set,NULL);
//创建套接字,绑定
int lfd = tcp4bind(8008,NULL);
//监听
Listen(lfd,128);
//提取
//回射
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
while(1)
{
char ip[16]="";
//提取连接,
int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);
printf("new client ip=%s port=%d\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16),
ntohs(cliaddr.sin_port));
//fork创建子进程
pid_t pid;
pid = fork();
if(pid < 0)
{
perror("");
exit(0);
}
else if(pid == 0)//子进程
{
//关闭lfd
close(lfd);
while(1)
{
char buf[1024]="";
int n = read(cfd,buf,sizeof(buf));
if(n < 0)
{
perror("");
close(cfd);
exit(0);
}
else if(n == 0)//对方关闭
{
printf("client close\n");
close(cfd);
exit(0);
}
else
{
printf("%s\n",buf);
write(cfd,buf,n);
// exit(0);
}
}
}
else//父进程
{
close(cfd);
//回收
//注册信号回调
struct sigaction act;
act.sa_flags =0;
act.sa_handler = free_process;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD,&act,NULL);
sigprocmask(SIG_UNBLOCK,&set,NULL);
}
}
//关闭
return 0;
}
函数大写说明是封装函数,有出错判断处理
7.2.多线程并发服务器
在使用线程模型开发服务器时需考虑以下问题:
调整进程内最大文件描述符上限
线程如有共享数据,考虑线程同步
服务于客户端线程退出时,退出处理。(退出值,分离态)
系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU
#include <stdio.h>
#include <pthread.h>
#include "wrap.h"
//在回调函数中传入两个参数
typedef struct c_info
{
int cfd;
struct sockaddr_in cliaddr;
}CINFO;
void* client_fun(void *arg);
int main(int argc, char *argv[])
{
if(argc < 2)//自行输入端口
{
printf("argc < 2??? \n ./a.out 8000 \n");
return 0;
}
//回收资源
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);//设置线程分离
short port = atoi(argv[1]);
int lfd = tcp4bind(port,NULL);//创建套接字 绑定
Listen(lfd,128);
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
CINFO *info;
while(1)
{
int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);
char ip[16]="";
pthread_t pthid;
info = malloc(sizeof(CINFO));//防止调度问题子线程覆盖,创建堆
info->cfd = cfd;
info->cliaddr= cliaddr;
pthread_create(&pthid,&attr,client_fun,info);
}
return 0;
}
void* client_fun(void *arg)
{
//把上面的CINFO结构体取下来
CINFO *info = (CINFO *)arg;
char ip[16]="";
printf("new client ip=%s port=%d\n",inet_ntop(AF_INET,&(info->cliaddr.sin_addr.s_addr),ip,16),
ntohs(info->cliaddr.sin_port));
while(1)
{
char buf[1024]="";
int count=0;//计数
count = read(info->cfd,buf,sizeof(buf));
if(count < 0)
{
perror("");
break;
}
else if(count == 0)
{
printf("client close\n");
break;
}
else
{
printf("%s\n", buf);
write(info->cfd,buf,count);
}
}
close(info->cfd);
free(info);//释放
}
7.3 client代码
/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (fgets(buf, MAXLINE, stdin) != NULL) {
Write(sockfd, buf, strlen(buf));
n = Read(sockfd, buf, MAXLINE);
if (n == 0) {
printf("the other side has been closed.\n");
break;
} else
Write(STDOUT_FILENO, buf, n);
}
Close(sockfd);
return 0;
}
8.本章扩展
0.大端小端
1.概念
大端模式(Big-endian):高位字节排放在内存的低地址端,低位字节排放在内存的高地址端,即正序排列,高尾端;
小端模式(Little-endian):低位字节排放在内存的低地址端,高位字节排放在内存的高地址端,即逆序排列,低尾端;
例(无论是小端模式还是大端模式。每个字节内部都是按顺序排列):
1)大端模式:
低地址 -----------------> 高地址
0x0A | 0x0B | 0x0C | 0x0D
2)小端模式:
低地址 ------------------> 高地址
0x0D | 0x0C | 0x0B | 0x0A
3)下面是两个具体例子:
16bit宽的数0x1234在两种模式CPU内存中的存放方式(假设从地址0x4000开始存放)为:
内存地址 |
小端模式存放内容 |
大端模式存放内容 |
0x4000 |
0x34 |
0x12 |
0x4001 |
0x12 |
0x34 |
32bit宽的数0x12345678在两种模式CPU内存中的存放方式(假设从地址0x4000开始存放)为:
内存地址 |
小端模式存放内容 |
大端模式存放内容 |
0x4000 |
0x78 |
0x12 |
0x4001 |
0x56 |
0x34 |
0x4002 |
0x34 |
0x56 |
0x4003 |
0x12 |
0x78 |
4)大端小端没有谁优谁劣,各自优势便是对方劣势:
小端模式 :强制转换数据不需要调整字节内容,1、2、4字节的存储方式一样。
大端模式 :符号位的判定固定为第一个字节,容易判断正负。
2.数组在大端小端模式下的存储:
以unsigned int num = 0x12345678为例,分别看看在两种字节序下其存储情况,我们可以用unsigned char buf[4]来表示num:
Big-Endian: 低地址存放高位,如下:
低地址
buf[0] (0x12) – 高位
buf[1] (0x34)
buf[2] (0x56)
buf[3] (0x78) – 低位
高地址
Little-Endian: 低地址存放低位,如下:
低地址
buf[0] (0x78) – 低位
buf[1] (0x56)
buf[2] (0x34)
buf[3] (0x12) – 高位
高地址
1.TCP状态转换
CLOSED:表示初始状态。
LISTEN:状态表示服务器端的某个SOCKET处于监听状态,可以接受连接。
SYN_SENT:这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,随即进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
SYN_RCVD: 该状态表示接收到SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂。此种状态时,当收到客户端的ACK报文后,会进入到ESTABLISHED状态。
ESTABLISHED:表示连接已经建立。
FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。
区别是:
FIN_WAIT_1状态是当socket在ESTABLISHED状态时,想主动关闭连接,向对方发送了FIN报文,此时该socket进入到FIN_WAIT_1状态。
FIN_WAIT_2状态是当对方回应ACK后,该socket进入到FIN_WAIT_2状态,正常情况下,对方应马上回应ACK报文,所以FIN_WAIT_1状态一般较难见到,而FIN_WAIT_2状态可用netstat看到。
FIN_WAIT_2:主动关闭链接的一方,发出FIN收到ACK以后进入该状态。称之为半连接或半关闭状态。**该状态下的socket只能接收数据,不能发。
TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,等2MSL后即可回到CLOSED可用状态。如果FIN_WAIT_1状态下,收到对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
CLOSING: 这种状态较特殊,属于一种较罕见的状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
CLOSE_WAIT: 此种状态表示在等待关闭。当对方关闭一个SOCKET后发送FIN报文给自己,系统会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,察看是否还有数据发送给对方,如果没有可以 close这个SOCKET,发送FIN报文给对方,即关闭连接。所以在CLOSE_WAIT状态下,需要关闭连接。
LAST_ACK: 该状态是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,即可以进入到CLOSED可用状态。
2.半关闭
主动方发生在FIN_WAIT_2状态,这个状态时,主动方不可以在应用层发送数据了,但是应用层还可以接收数据,这个状态称为半关闭
#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd: 需要关闭的socket的描述符
how: 允许为shutdown操作选择以下几种方式:
SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。
SHUT_RDWR(2): 关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。
使用close中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0时才关闭连接。
shutdown不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。
注意:
- 如果有多个进程共享一个套接字,close每被调用一次,计数减1,直到计数为0时,也就是所用进程都调用了close,套接字将被释放。
- 在多进程中如果一个进程调用了shutdown(sfd, SHUT_RDWR)后,其它的进程将无法进行通信。但,如果一个进程close(sfd)将不会影响到其它进程。
3. 心跳包
如果对方异常断开,本机检测不到,一直等待,浪费资源需要设置tcp的保持连接,作用就是每隔一定的时间间隔发送探测分节,如果连续发送多个探测分节对方还未回,就将次连接断开
int keepAlive = 1;
setsockopt(listenfd, SOL_SOCKET, SO_KEEPALIVE, (void*)&keepAlive, sizeof(keepAlive));
心跳包: 最小粒度乒乓包: 携带比较多的数据的心跳包
4 .设置端口复用
在server代码的socket()和bind()调用之间插入如下代码:
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));//放到绑定前
注意: 程序中设置某个端口重新使用,在这之前的其他网络程序将不能使用这个端口
本文福利, 免费领取Linux C/C++全栈开发学习资料包、技术视频/代码,1000道大厂面试题,内容包括(C++基础,网络编程,数据库,中间件,后端开发,音视频开发,Qt开发,游戏开发,Linux内核等进阶学习资料和最佳学习路线图)↓↓↓↓有需要的可以进企鹅裙927239107领取哦~↓↓