Linux网络编程
1.socket编程
socket是一种通信机制,用于在网络中不同计算机之间进行数据传输,当然也可用用于进程间通信。在linux中,有文件描述符这么个东西,我们可以通过socket函数创建一个网络连接,socket的返回值为一个文件描述符,我们拿到这个文件描述符就可以像操作普通io文件那样来操作函数传输数据。
调用socket函数时,会创建两个缓冲区,与管道不同的时socket使用一个文件描述符来管理两个缓冲区。
1.1 socket编程的基础知识
网络字节序:
- 大端(网络字节序):低位地址存放高位数据,高位地址存放低位数据。
- 小端(主机字节序):低位地址存放低位数据,高位地址存放高位数据。
在实际的开发中,我们需要进行大小端的转换,下面是大小端的转换函数:
头文件:#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位无符号整数大端到小端的转换(网络-\>主机)。
IP地址转换函数:
头文件:#include <arpa/inet.h>
函数原型:int inet_pton(int af, const char *src, void *dst);
函数功能:将字符串形式的点分十进制的IP地址转换为16进制数。
函数参数:
af:填AF_INET代表转ipv4,填AF_INET6代表转ipv6
src:字符串形式的点分十进制的IP地址。
dst:存放转换后的变量地址。
函数返回值:成功返回1,失败返回0,发生错误返回-1。
函数原型:const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
函数功能:将网络IP地址转换为字符串形式的点分十进制的IP。
函数参数:
af:填AF_INET代表转ipv4,填AF_INET6代表转ipv6
src:网络IP地址。
dst:存放转换后的字符串形式的ip地址。
size:填dst的长度。
函数返回值:成功返回指向dst的指针,失败返回NULL。
socket编程中一些的重要结构体:
结构体struct sockaddr的成员变量的说明:
unsigned short sa_family:表示地址组,填AF_INET表示使用ipv4,填AF_INET6表示使用ipv6.
char sa_data[14]:用来存储具体的地址数据。
结构体struct sockaddr_in的成员变量的说明:
short sin_family:表示地址组,填AF_INET表示使用ipv4,填AF_INET6表示使用ipv6.
unsigned short sin_port:表示端口号,使用网络字节序(大端序)。
struct in_addr sin_addr:表示ipv4地址,是一个结构体,用来存储32位的ipv4地址。
char sin_zero[8]:填充字段,一般设置为0。
memset函数:
函数原型:void *memset(void *ptr, int value, size_t num);
函数功能:用于将一段内存块中的内容设置为指定的值。
函数参数:
ptr:指向要设置值的内存块指针。
value:要设置的值,以整型形式传入。
num:要设置的字节数。
函数返回值:返回一个指向ptr指针。
1.2 socket编程的主要函数:
socket编程需要包含以下头文件:
- #include <sys/types.h>
- #include <sys/socket.h>
1.2.1 socket函数
函数原型:int socket(int domain, int type, int protocol);
函数功能:创建socket。
函数参数:
domain:协议版本。
- AF_INET 使用IPV4
- AF_INET6 使用IPV6
- AF_UNIX AF_LOCAL使用本地套接字使用
type:协议类型。
- SOCK_STREAM 流式, 默认使用的协议是TCP协议
- SOCK_DGRAM 报式, 默认使用的是UDP协议
protocal:一般填0,表示使用对应的默认协议。
函数返回值:成功返回一个大于0的文件描述符,失败返回-1。
调用成功以后,会返回一个文件描述符,内核会提供与该文件描述符对应读和写的缓冲区,还有两个队列,分别是请求链接队列和已链接队列。
1.2.2 bind函数
函数原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数功能:将文件描述符和IP,端口号绑定到一个套接字。
函数参数:
sockfd:要绑定的套接字的文件描述符,就是socket返回的那个。
addr:指向要绑定的本地地址结构体的指针。
addrlen:表示本地结构体的长度,使用sizeof(struct sockaddr)来获取。
函数返回值:成功返回0,失败返回-1。
1.2.3 listen函数
函数原型:int listen(int sockfd, int backlog);
函数功能:将套接字由主动状态转变为被动监听状态。
函数参数:
sockfd:要将那个套接字转变为监听态。
backlog:同时请求的最大个数(表示等待队列的最大长度)。
函数返回值:成功返回0,失败返回-1。
1.2.4 accept函数
函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
函数功能:接受传入的请求,并且新建一个新的套接字,用于与客户端之间进行通信。没有链接会阻塞。
函数参数:
sockfd:表示监听套接字的文件描述符。
addr:是一个指针,用于存储客户端的地址信息。
addrlen:用于指定addr的长度。
函数返回值:成功返回一个新的套接字,用于与客户端进行通信。失败返回-1。
accept函数是一个阻塞函数, 若没有新的连接请求, 则一直阻塞.
从已连接队列中获取一个新的连接, 并获得一个新的文件描述符, 该文件描述符用于和客户端通信. (内 核会负责将请求队列中的连接拿到已连接队列中)
1.2.5 connect函数
函数原型:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数功能:连接服务器。
函数参数:
sockfd:调用socket返回的文件描述符。
addr:服务端的地址信息。
addrlen:addr变量的内存大小。
函数返回值:成功fanhui0,失败返回-1。
接下来就可以使用write和read函数进行读写操作了,当然还可以使用recvhesend函数。但是使用时不 要混着使用,要么读写都用write和read或recv和send,不要混着用。
1.2.6 send函数
函数原型:ssize_t send(int sockfd, const void *buf, size_t len, int flags);
函数功能:将数据从已连接的套接字发送给对方。
函数参数:
sockfd:表示已建立链接的套接字描述符。
buf:指向要发送数据的缓冲区指针。
len:要发送数据的长度。
flags:标志参数,一般填0;
函数返回值:成功会返回刚才发送的字节数,失败会返回-1。
1.2.7 recv函数
函数原型:ssize_t recv(int sockfd, void *buf, size_t len, int flge);
函数功能:从已连接的套接字中接收数据。
函数参数:
sockfd:已连接的套接字描述符。
buf:用于存放接收的数据。
len:存放数据容器的长度。
flags:参数标志,一般填0。
函数返回值:成功返回接收的字节数,失败返回-1。
1.3 使用socketAPI函数编写服务端和客户端的程序
1.3.1 服务端程序步骤:
- 第一步创建套接字:使用socket函数创建一个套接字,同时指定地址族,套接字类型和协议。
- 第二步绑定地址:使用bind函数将套接字绑定到一个本地地址,同时指定监听的端口号和ip地址。
- 第三步设置监听:使用listen函数将套接字设置为被动监听状态,指定同时等待处理的链接请求数量。
- 第四步接受连接:使用accept函数接受传入的连接请求,并且accept会创建一个新的套接字用于与客户端通信。
- 第五步与客户端通信:通过accept给出的套接字与客户端通信,可以使用send和recv或read和write函数发送和接收数据。
- 第六步关闭套接字:当通信结束后,使用close()函数关闭给出accept和socket两个函数给出的套接字。
1.3.2 客户端程序步骤:
- 第一步创建套接字:使用socket函数创建一个套接字,同时指定地址族,套接字类型和协议。
- 第二步连接服务器:使用connect函数连接到服务端地址,同时指定服务端的IP地址和端口号。
- 第三步与服务端通信:连接成功后,可以使用send和recv或read和write函数与服务端交换数据。
- 第四步关闭套接字:当通信结束后,使用close关闭刚才socket的套接字。
1.4 服务端/客户端的源代码示例
1.4.1 服务端代码:
点击查看代码
#include <stdio.h >
#include <stdlib.h >
#include <sys/types.h >
#include <sys/socket.h >
#include <netinet/in.h >
#include <string.h >
int main()
{
int fd;//用于记录socket创建的套接字
int netfd;//用于记录accept给出的套接字
//用于存储ip和端口号等网络信息,serv_addr存服务端的,cli_addr存客户端的
struct sockaddr_in serv_addr, cli_addr;
socklen_t len;//代表地址数据结构体的长度
//创建套接字,指定使用ipv4,tcp协议
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
perror("套接字创建失败,请重试!");
return -1;
}
//准备本地地址结构体
serv_addr.sin_family = AF_INET; //使用ipv4
serv_addr.sin_port = htons(10066); //端口号为10066(取值:1024到65535),使用网络字节序
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //ip地址,INADDR_ANY表示任意ip
//确保 sin_zero 数组中的所有字节都被初始化为 0,以防止其中包含任何垃圾数据
memset(serv_addr.sin_zero, ' 0', sizeof(serv_addr.sin_zero));
//将套接字描述符与本地地址绑定,bind接收sockaddr,所以需要强制将sockaddr_in类型的serv转换一下
if (bind(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
perror("服务端与本地地址绑定失败!");
close(fd); //因为已经创建了套接字,所以失败了记得关闭
return -1;
}
//将套接字设置为监听态
if (listen(fd, 10) == -1) {
perror("未能完成监听状态的转换!");
close(fd); //因为已经创建了套接字,所以失败了记得关闭
return -1;
}
len = sizeof(cli_addr);
//使用accept接受链接
netfd = accept(fd, (struct sockaddr *)&cli_addr, &len);
if (netfd == -1) {
perror("未能创建链接!");
close(fd); //因为已经创建了套接字,所以失败了记得关闭
return -1;
}
char data[1000]; //用于存储客户端发来的信息
char serv_data[100]; //用于存储要发给客户端的信息
int num_bytes; //用来存储recv的返回值,如果返回值异常则进行相关操作
//使用无限循环持续监听
while (1) {
//接收信息
num_bytes = recv(netfd, data, sizeof(data), 0);
if (num_bytes <= 0) {
perror("信息接收失败!可能是是对方关闭了链接");
break;//记得跳出循环
}
//处理收到的数据,这里选择打印,你可以选择做其他操作
printf("来自客户端:%s \n",data);
//向客户端发送响应数据
if (send(netfd, serv_data, strlen(serv_data), 0) == -1) {
perror("响应发送失败!");
break;
}
}
//关闭套接字
close(fd); //关闭socket返回的套接字
close(netfd); //关闭accept返回的套接字
return 0;
}
1.4.2客户端代码:
点击查看代码
#include <stdio.h >
#include <stdlib.h >
#include <sys/types.h >
#include <sys/socket.h >
#include <netinet/in.h >
#include <arpa/inet.h >
#include <string.h >
int main()
{
int fd;//用于记录socket返回的文件描述符
//存储服务端地址信息,用于链接服务端使用
struct sockaddr_in serv_addr;
//创建套接字,使用ipv4hetcp协议
fd = socket(AF_INET,SOCK_STREAM, 0);
if (fd == -1) {
perror("客户端创建套接字失败!");
return -1;
}
//准备初始化服务器地址结构体
serv_addr.sin_family = AF_INET; //使用ipv4
serv_addr.sin_port = htons(10066); //端口号,使用网络字节序
//服务器使用网络字节序(大端),我们需要将主机字节序(小端)转为网络字节序(大端)
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("未能将主机字节序转为网络字节序");
close(fd); //因为已经创建了套接字,所以失败了记得关闭
return -1;
}
//初始化服务器的地址信息为0,避免有其他乱七八糟的东西
memset(serv_addr.sin_zero, ' 0', sizeof(serv_addr.sin_zero));
//连接服务器
if (connect(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
perror("连接服务器失败!");
close(fd); //因为已经创建了套接字,所以失败了记得关闭
return -1;
}
//发送数据
char data[1000]; //用于存储要发送的数据
printf("请输入要发生的数据: \n");
scanf("%s", &data);//输入要发送的数据
if (send(fd, data, strlen(data), 0) == -1) {
perror("发送失败!");
close(fd); //因为已经创建了套接字,所以失败了记得关闭
return -1;
}
//接收相应数据
int num_bytes; //用于接收recv函数的值,通过判断返回值作出相应的操作
num_bytes = recv(fd, data, sizeof(data), 0);
if (num_bytes <= 0) {
perror("接收失败!");
close(fd); //因为已经创建了套接字,所以失败了记得关闭
return -1;
}
//成功接收后num_bytes就是接收的数据的大小,记得在最后加上' 0'
data[num_bytes] = ' 0'; //加上' 0是为了截断字符串,' 0'后面的内容没用'
printf("来自服务端:%s \n", data);
close(fd);//关闭socket的文件描述符
return 0;
}
1.5高并发服务器
之前的服务器代码,服务端只能同时接收一个客户端的连接,如果想要服务端支持多个客户端的连接请求就可以使用多进程或多线程的方式来接收多个客户端的请求。
1.5.1多进程实现高并发的步骤:
- 第一步:创建socket,使用socket函数创建一个用于监听的套接字描述符。
- 第二步:使用bind函数将监听套接字描述符与本地的ip和端口进行绑定。
- 第三步:将第一步是套接字描述符设置为监听状态。使用listen函数。
- 第四步:使用一个无限循环,在循环中不断接收新的客户端的连接请求。
- 第五步:使用accept函数来接收客户端的请求,并且得到与客户端建立了连接的套接字描述符。
- 第六步:创建子进程,通过判断子进程pid的方式来进行具体的操作(与客户端通信等待操作)。
- pid == 0:代表子进程正在运行,因为子进程创建会继承父进程的套接字描述符,为了防止子进程误用该描述符导致产生竞争条件等等未意料的行为,需要将继承的监听套接字关闭,然后再通过刚才继承的用于和客户端通信的套接字与客户端通信并且执行相应的操作,处理客户端的请求也需要套用无限循环,这样的作用是可以反复地处理同一个客户端的请求,当该客户端关闭链接时可以通过某些手段(如判断recv的返回值)来确定是否要跳出循环并且关闭与该客户端通信的套接字描述符,之后记得退出进程。
- pid > 0:代表当前是父进程执行到了这里,当父进程使用fork函数创建了子进程之后,假设父进程有一个通信套接字client,那么子进程也会继承这个通信套接字,如果父进程调用fork函数,返回值会是一个大于0的整数,这个整数是子进程的id,既然父进程能够将代码执行到这里,那么父进程调用fork函数的返回值一定是大于0的,也意味着子进程创建成功,因为子进程会继承通信套接字client与客户端交流,说白了就是子进程接管了这个通信套接字,所以父进程中已有的通信套接字client可以关闭掉,这样的话可以节省系统资源,防止资源泄漏。
- pid < 0 :代表子进程创建失败!
- 第七步:重复第六步,直到我们不需要接收客户端连接请求了,可以调用close函数关闭监听套接字,然后关闭服务端程序。
1.5.2 多线程实现高并发的步骤:
- 第一步:创建socket,使用socket函数创建一个用于监听的套接字描述符。
- 第二步:使用bind函数将监听套接字描述符与本地的ip和端口进行绑定。
- 第三步:将第一步是套接字描述符设置为监听状态。使用listen函数。
- 第四步:使用一个无限循环,然后在循环中使用accept函数不断接收请求,并且创建线程,然后让线程执行相应的操作,记得使用detach方法来分离线程,千万别让主线程阻塞。记得在线程执行完操作之后关闭通信套接字描述符,因为子线程会有自己独立的通信描述符,用完了记得关闭。如果不用接收数据了,可以使用某些方法跳出这个循环。
- 第五步;重复第四步,当不需要接收客户端的链接时,可以通过某些条件跳出循环,然后关闭监听套接字。
- 第二步:使用bind函数将监听套接字描述符与本地的ip和端口进行绑定。
1.6 高并发服务器的源代码示例:
1.6.1 多进程版本:
点击查看代码
#include <stdio.h >
#include <stdlib.h >
#include <sys/types.h >
#include <sys/socket.h >
#include <netinet/in.h >
#include <string.h >
#include <unistd.h >
int DisposeClient(int clinetSocket);
int main()
{
int servSock, clieSock;//分别记录服务端和客户端的socket
struct sockaddr_in clieAddr;
struct sockaddr_in servAddr; //分别记录服务端和客户端的地址信息
socklen_t addrSize; //地址信息的大小
pid_t childPid; //记录子进程的pid
//创建套接字,使用ipv4和tcp协议
servSock = socket(AF_INET, SOCK_STREAM, 0);
//绑定本地地址和端口
bzero(&servAddr, sizeof(servAddr));
servAddr.sin_family = AF_INET; //使用ipv4
servAddr.sin_port = htons(10066); //端口为10066
servAddr.sin_addr.s_addr = htonl(INADDR_ANY); //表示本地任意地址
memset(servAddr.sin_zero, ' 0', sizeof(servAddr.sin_zero));
//绑定
bind(servSock, (struct sockaddr *)&servAddr, sizeof(servAddr));
//切换套接字为监听态,并且同时支持10个连接
if (listen(servSock, 100) == 0) {
printf("切换监听模式成功! \n");
}
else {
printf("切换监听模式失败! \n");
exit(1);
}
//循环监听
while (1) {
//接受连接
addrSize = sizeof(clieAddr);
clieSock = accept(servSock, (struct sockaddr *)&clieAddr, &addrSize);
if (clieSock < 0) {
//连接失败,等待下一个连接
continue;
}
//创建子进程,用来处理客户端连接
childPid = fork();
if (childPid == 0) {
//子进程在运行
close(servSock);//关闭监听套接字
while (1) {
//处理客户端请求
if (DisposeClient(clieSock) == -1) {
break;
}
}
close(clieSock); //关闭通信套接字
exit(0);//子进程退出
}
else if (childPid > 0) {
close(clieSock); //父进程关闭通信套接字
}
else if (childPid < 0) {
//fork未能创建子进程
exit(-1);
}
}
//关闭服务端套接字
close(servSock);
return 0;
}
int DisposeClient(int clinetSocket)
{
char data[256];
char serv_data[100] = "服务器已收到信息。";
int num_bytes;
num_bytes = recv(clinetSocket, data, sizeof(data), 0);
if (num_bytes <= 0) {
perror("信息接收失败!可能是对方关闭了链接 \n");
return -1;
}
//处理收到的数据,这里选择打印,你可以选择做其他操作
printf("来自客户端:%s \n", data);
//向客户端发送响应数据
if (send(clinetSocket, serv_data, strlen(serv_data), 0) == -1) {
perror("响应发送失败!");
}
return 0;
}
1.6.2 多线程版本:
点击查看代码
#include <stdio.h >
#include <stdlib.h >
#include <sys/types.h >
#include <sys/socket.h >
#include <netinet/in.h >
#include <string.h >
#include <unistd.h >
#include <pthread.h >
void *thread_work(void *arg);
int main()
{
//创建socket,使用ipv4和tcp协议
int sfd = socket(AF_INET, SOCK_STREAM, 0);
//绑定
struct sockaddr_in serv;
serv.sin_family = AF_INET;
serv.sin_port = htons(10066);
serv.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sfd, (struct sockaddr*)&serv, sizeof(serv));
//设置监听
if (listen(sfd, 100) == 0) {
printf("切换监听模式成功! \n");
}
else {
printf("切换监听模式失败! \n");
exit(1);
}
int cfd;
pthread_t threadID;
while (1) {
//接收新链接
cfd = accept(sfd, NULL, NULL);
//创建子线程
pthread_create(&threadID, NULL, thread_work, &cfd);
//设置子线程为分离属性
pthread_detach(threadID);
//注意:这里死循环,它会一直监听链接,需要你自己想办法通过某些条件跳出
}
//关闭监听文件描述符
close(sfd);
return 0;
}
void *thread_work(void *arg)
{
int cfd = *(int *)arg;
char data[100];
char serv_data[100] = "服务器已经收到来自客户端的消息!";
int n;
while (1) {
//读数据
n = recv(cfd,data,sizeof(data),0);
if (n <= 0) {
printf("读取错误!");
break;
}
//处理收到的数据,这里选择打印,你可以选择做其他操作
printf("来自客户端:%s \n", data);
//回应客户端
if (send(cfd, serv_data, strlen(serv_data), 0) == -1) {
printf("回应失败!");
}
}
close(cfd); //关闭通信描述符
pthread_exit(NULL); //退出子线程
}