字节序
1、概述
什么是字节序:
字节序就是字节的存储顺序,分为大端字节序和小端字节序。
- 大端字节序:低地址存高位(网络)
- 小端字节序:低地址存低位(主机)
检验主机字节序模式:
#include <stdio.h>
int main(){
unsigned int a = 0x11223344;
unsigned char b = *((unsigned char *)&a);
if(b == 0x44){
printf("小端字节序\n");
}else{
printf("大端字节序\n");
}
return 0;
}
2、字节序转换
2.1 端口号字节序转换函数
因为主机使用的是小端字节序,网络使用的是大端字节序,因此在数据传输时,需要先将数据转换为大端字节序传给网络,之后再将数据转换为小端字节序传给另一个主机。
/* h:本机 n:网络 l:32位 s:16位 */
//32位数据(4字节)
uint32_t htonl(uint32_t hostlong);//本机->网络
uint32_t ntohl(uint32_t netlong);//网络->本机
//16位数据(2字节)
uint16_t htons(uint16_t hostshort);//本机->网络
uint16_t ntohs(uint16_t netshort);//网络->本机
返回值:将小端字节序转换为大端字节序后的端口号
参数:端口号值
2.2 IP地址字节序转换函数
该函数主要是实现点分十进制表示的IP地址转换,而不需要一个字节一个字节的去转换。
2.2.1 IPv4
//点分十进制字符串->网络字节序
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
//网络字节序->点分十进制字符串
char *inet_ntoa(struct in_addr in);
inet_addr返回值:ip地址
cp:点分十进制字符串,例如传入"192.168.1.1"这个字符串
inp:ip地址
inet_ntoa返回值:点分十进制字符串
in:ip地址
2.2.1 IPv6
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
套接字socket
概述
socket多种含义:
- 应用编程接口API:socket API,简称socket。
- 函数名:socket API中有一个名为socket的函数
- 端点:比如TCP连接是有两个端点,这两个端点是一对一通信的关系。这个端点也叫socket
- 文件描述符:socket函数的返回值是一个socket描述符,简称socket
socket的作用:
socket处在应用层与内核之间。在应用层中实现的是与应用相关的代码,在内核中实现的是网络通信相关的代码。在OSI结构中,应用层就是OSI的应用层、表示层、会话层,内核就是运输层、网络层、数据链路层、物理层。
什么是三元组:
三元组指的是IP地址、端口号、协议。该数据通过bind函数进行绑定。
- IP地址:标识计算机,找到与网络中的哪一个计算机进行通信。
- 端口号:标识进程,找到与计算机中哪一个进程进行通信。
- 协议:指定数据以什么样的方式进行传递。主要指TCP、UDP
套接字的类型:
- 流式套接字 (SOCK_STREAM) :提供可靠的、面向连接的通信流;它使用TCP,从而保证数据传输的可靠性和顺序性
- 数据报套接字 (SOCK_DGRAM) :定义了一种不可靠、面向无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议UDP
- 原始套接字(SOCK_RAW) :允许直接访问底层协议,如IP或ICMP,它功能强大但使用较为不便,主要用于协议开发。
socket文件的读写含义:
socket在Linux中也是一种文件,对socket文件进行读就是读取网络传输过来的数据,对socket文件进行写就是向网络中传输相应的数据。
相关函数
socket相关API:
创建套接字、绑定通信结构体、监听套接字、接收套接字、发起连接
通用
1、创建套接字
int socket(int domain, int type, int protocol);
返回值:成功返回socket文件描述符,失败返回-1
domain:指定bind中传入的地址族结构体的类型,与sa_family_t的取值要一致。
domain值 | 含义 |
AF_UNIX | UNIX 域套接字地址族,用于在同一台主机上的进程间通信 |
AF_INET | IPv4 地址族,用于 IPv4 通信 |
AF_INET6 | IPv6 地址族,用于 IPv6 通信 |
type:套接字的类型,就是"概述"中的说的三种类型
type值 | 使用的协议 |
SOCK_STREAM | TCP |
SOCK_DGRAM | UDP |
SOCK_RAW |
protocol:协议。在TCP、UDP时,该值写0。type = SOCK_RAW时需要根据需求选择该值。
2、绑定通信结构体
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
返回值:成功返回0,失败返回-1
sockfd:socket文件描述符
addr:通用地址族结构体,存放了通信地址的结构、端口号、IP地址这三个信息
addrlen:传入的addr结构体的宽度,用sizeof求出
struct sockaddr结构体:
//通用地址族结构体
struct sockaddr {
sa_family_t sa_family; //通信地址的结构
char sa_data[14];
}
sa_family:代表通信地址的结构,取值及含义如下
sa_family值 | 含义 |
AF_UNIX | UNIX 域套接字地址族,用于在同一台主机上的进程间通信 |
AF_INET | IPv4 地址族,用于 IPv4 通信 |
AF_INET6 | IPv6 地址族,用于 IPv6 通信 |
struct sockaddr_in结构体:
该结构体用于捆绑IPv4的地址。
struct sockaddr_in {
sa_family_t sin_family; //通信地址的结构
in_port_t sin_port; //端口号
struct in_addr sin_addr; //IP地址(这是个结构体)
};
struct in_addr {
in_addr_t s_addr; //IP地址
};
struct sockaddr_in6结构体:
该结构体用于捆绑IPv6的地址。
struct sockaddr_in6 {
sa_family_t sin6_family; /* Address family (AF_INET6) */
in_port_t sin6_port; /* Port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
struct in6_addr {
uint8_t s6_addr[16]; /* IPv6 address in network byte order */
};
TCP
1、监听套接字
int listen(int sockfd, int backlog);
返回值:成功返回0,失败返回-1
sockfd:socket文件描述符
backlog:允许接入的客户端的总数量
该函数应用于服务器, 调用完该函数后,会创建一个指定长度的队列,用于存储接收到的客户端。
2、接收套接字
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
返回值:成功新的socket文件描述符,用于与客户端进行连接,失败返回-1
后面数据交互用的是该文件描述符,而不是socket函数返回的文件描述符。
sockfd:socket文件描述符
addr:通用地址族结构体,该参数是传出的参数,是接入服务器的客户端的信息。
addrlen:addr结构体的宽度,该参数是传出的参数
该函数应用于客户端,是从listen创建的队列中出队一个客户端,并生成一个新的socket与这个客户端进行连接。
注意:一个accept只能接入一个客户端,如果想要接入多个客户端,那么应该把accept写入到while循环中
3、发起连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
返回值:成功返回0,失败返回-1
sockfd:socket文件描述符
addr:通用地址族结构体,该参数是要连接的服务器的信息
addrlen:addr结构体的宽度,sizeof求出
4、获取socket信息
//获取自己的socket信息
int getsockname(int socket, struct sockaddr *restrict address,
socklen_t *restrict address_len);
//获取对端的socket信息
int getpeername(int socket, struct sockaddr *restrict address,
socklen_t *restrict address_len);
返回值:成功返回0,失败返回-1
socket:写入accept返回的socket文件描述符
其余参数与accept的参数一致
UDP
1、接收/发送数据
//发送
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
//接收
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd:socket文件描述符
buf:缓冲区
len:数据长度
flag:标志位,一般设置为0。具体含义如下
flag值 | 含义 |
0 | send相当于write recv相当于read(读后数据从缓冲区删除) |
MSG_PEEK | 窥视传入的数据。 数据被复制到缓冲区,但不会从输入队列中删除 |
MSG_OOB | 处理带外数据 |
//发送
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
//接收
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
第一行参数:sockfd、buf、len、flags与send、recv函数的参数含义一致
dest_addr:发送给哪一个设备,包含设备的IP、端口号
src_addr:接收到的是哪一个设备的信息,包含设备的IP、端口号
addrlen:长度,用sizeof 求出
TCP
测试
1、使用ifconfig获取环回IP地址
2、使用 nc <IP地址> <端口号>使得终端作为客户端连接服务器(这里服务器端口号为8888)
单线程
TCP客户端与服务端实现步骤:
server.c代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc ,char** argv){
int fd;
struct sockaddr_in addr;
//判断参数有效性
if(argc != 3){
printf("param err\n");
printf("%s<ip><port>\n",argv[0]);
return -1;
}
printf("ip = %s\n",argv[1]);
printf("port = %s\n",argv[2]);
//1.创建socket
if((fd=socket(AF_INET,SOCK_STREAM,0))<0){//IPv4,TCP协议
perror("socket");
return -1;
}
//2.绑定IP、端口号
addr.sin_family = AF_INET; //IPv4
addr.sin_port = htons(atoi(argv[2])); //端口号,要转化为大端子节序
addr.sin_addr.s_addr = inet_addr(argv[1]); //IP地址:0表示在本网络上的本主机,即:自己
if(bind(fd,(struct sockaddr*)&addr,sizeof(struct sockaddr_in)) == -1){
perror("bind");
return -1;
}
//3.监听socket
if(listen(fd,5) == -1){ //允许最多接入5个客户端
perror("listen");
return -1;
}
//4.接受客户端链接
int newFd;
struct sockaddr_in newAddr;
socklen_t newAddrlen;
if((newFd = accept(fd,(struct sockaddr*)&newAddr,&newAddrlen)) < 0){
perror("accept");
return -1;
}
//5.数据交互
printf("client port = %d\n",ntohs(newAddr.sin_port));
printf("client ip = %s\n",inet_ntoa(newAddr.sin_addr));
while(1){
write(newFd,"server",strlen("server\n"));
sleep(1);
}
close(newFd);
close(fd);
return 0;
}
client.c代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc,char** argv){
int fd;
struct sockaddr_in addr;
//判断参数有效性
if(argc != 3){
printf("param err\n");
printf("%s<ip><port>\n",argv[0]);
return -1;
}
printf("ip = %s\n",argv[1]);
printf("port = %s\n",argv[2]);
//1.创建socket
if((fd=socket(AF_INET,SOCK_STREAM,0))<0){//IPv4,TCP协议
perror("socket");
return -1;
}
//2.链接服务器
addr.sin_family = AF_INET; //IPv4
addr.sin_port = htons(atoi(argv[2])); //服务器端口号,要转化为大端子节序
addr.sin_addr.s_addr = inet_addr(argv[1]); //服务器IP地址:在本网络上的本主机,即:自己
if(connect(fd,(struct sockaddr*)&addr,sizeof(struct sockaddr_in)) == -1){
perror("connect");
return -1;
}
//3.数据交互
char buf[100] = {0};
while(1){
if(read(fd,buf,sizeof(buf)-1) > 0){
printf("read:%s\n",buf);
write(fd,"client:i read it\n",strlen("client:i read it\n"));
}
}
close(fd);
return 0;
}
代码执行结果:
并发
1、地址快速重用
当服务器与客户端正在处于连接状态,若服务器先于客户端关闭,之前服务器使用的端口号并不会立刻关闭。为了解决这个问题需要将以下代码添加到"建立套接字"代码之后。
int flag=1,len=sizeof(int);
if(setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&flag,len) == -1){
perror("setsockopt");
return -1;
}
2、多进程并发
多进程并发设计步骤:
- 在 "单线程" 的代码基础上,将accept函数和数据交互部分写入一个新的while循环,这代表服务器可以不断接收客户端。
- 在accept后创建子进程,子进程需要关闭socket返回的文件描述符,父进程需要关闭accept返回的文件描述符。之后子进程用来与接入的客户端进行通信,父进程继续等待新的客户端接入。
- 在accept前编写SIGCHLD信号处理相关函数,以便多进程时使用信号机制来回收子进程。
客户端退出情况分析:
- 正常交互:服务器收到客户端发来的退出指令退出交互的while,之后使用exit退出子进程
- 异常退出:客户端突然终止,服务器向客户端发送数据后会产生SIGPIPE信号,从而由信号终止子进程
server.c改进代码如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
void Set_SIGCHLD(void);
void SIGCHLD_Handler(int sig);
int main(int argc ,char** argv){
int fd;
struct sockaddr_in addr;
//判断参数有效性
if(argc != 3){
printf("param err\n");
printf("%s<ip><port>\n",argv[0]);
return -1;
}
printf("ip = %s\n",argv[1]);
printf("port = %s\n",argv[2]);
//1.创建socket
if((fd=socket(AF_INET,SOCK_STREAM,0))<0){//IPv4,TCP协议
perror("socket");
return -1;
}
//地址快速重用
int flag=1,len=sizeof(int);
if(setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&flag,len) == -1){
perror("setsockopt");
return -1;
}
//2.绑定IP、端口号
addr.sin_family = AF_INET; //IPv4
addr.sin_port = htons(atoi(argv[2])); //端口号,要转化为大端子节序
addr.sin_addr.s_addr = inet_addr(argv[1]); //IP地址:0表示在本网络上的本主机,即:自己
if(bind(fd,(struct sockaddr*)&addr,sizeof(struct sockaddr_in)) == -1){
perror("bind");
return -1;
}
//3.监听socket
if(listen(fd,5) == -1){ //允许最多接入5个客户端
perror("listen");
return -1;
}
//多进程并发
pid_t pid;
int newFd;
struct sockaddr_in newAddr;
socklen_t newAddrlen;
Set_SIGCHLD();//以信号方式回收子进程
while(1){
//4.接受客户端链接
if((newFd = accept(fd,(struct sockaddr*)&newAddr,&newAddrlen)) < 0){
perror("accept");
return -1;
}
//父进程处理接收客户端链接的问题
//子进程处理与客户端交互的问题
if((pid=fork()) == -1){
perror("fork");
return -1;
}
else if(pid == 0){
close(fd);//对于子进程,socket返回的fd没有用
//5.数据交互
printf("client port = %d\n",ntohs(newAddr.sin_port));
printf("client ip = %s\n",inet_ntoa(newAddr.sin_addr));
while(1){
write(newFd,"server",strlen("server\n"));
sleep(1);
}
exit(0);
}else{
close(newFd);//对于父进程,accept返回的newFd没有用
}
}
close(fd);
return 0;
}
void Set_SIGCHLD(void){
struct sigaction act;
act.sa_handler = SIGCHLD_Handler;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_RESTART;//让因为信号而终止的系统调用继续运行
if(sigaction(SIGCHLD,&act,NULL) != 0){
perror("sigaction");
}
}
void SIGCHLD_Handler(int sig){
int wstatus;
waitpid(-1,&wstatus,WNOHANG);
if(WIFEXITED(wstatus)){ //判断子进程是否正常退出
printf("子进程的返回值为%d\n",WEXITSTATUS(wstatus));
}else{
printf("子进程是否被信号结束%d\n",WIFSIGNALED(wstatus));
printf("结束子进程的信号类型%d\n",WTERMSIG(wstatus));
}
}
代码运行结果如下:
3、多线程并发
多进程并发设计步骤:
- 在 "单线程" 的代码基础上,将accept函数和数据交互部分写入一个新的while循环,这代表服务器可以不断接收客户端。
- 在accept后创建新的线程并进行线程分离,让新的线程去与客户端进行数据交互,原线程依旧等待新的客户端接入
存在问题:
未解决ctrl c终止客户端后,整个服务器因管道破裂信号而整个进程退出的情况
server.c改进代码如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <pthread.h>
void* clientThread(void* arg);
int main(int argc ,char** argv){
int fd;
struct sockaddr_in addr;
//判断参数有效性
if(argc != 3){
printf("param err\n");
printf("%s<ip><port>\n",argv[0]);
return -1;
}
printf("ip = %s\n",argv[1]);
printf("port = %s\n",argv[2]);
//1.创建socket
if((fd=socket(AF_INET,SOCK_STREAM,0))<0){//IPv4,TCP协议
perror("socket");
return -1;
}
//2.绑定IP、端口号
addr.sin_family = AF_INET; //IPv4
addr.sin_port = htons(atoi(argv[2])); //端口号,要转化为大端子节序
addr.sin_addr.s_addr = inet_addr(argv[1]); //IP地址:0表示在本网络上的本主机,即:自己
if(bind(fd,(struct sockaddr*)&addr,sizeof(struct sockaddr_in)) == -1){
perror("bind");
return -1;
}
//3.监听socket
if(listen(fd,5) == -1){ //允许最多接入5个客户端
perror("listen");
return -1;
}
//多线程并发
int newFd;
struct sockaddr_in newAddr;
socklen_t newAddrlen;
pthread_t tid;
while(1){
//4.接受客户端链接
if((newFd = accept(fd,(struct sockaddr*)&newAddr,&newAddrlen)) < 0){
perror("accept");
return -1;
}
printf("client port = %d\n",ntohs(newAddr.sin_port));
printf("client ip = %s\n",inet_ntoa(newAddr.sin_addr));
//创建线程
if(pthread_create(&tid,NULL,(void*)clientThread,(void*)newFd) != 0){
perror("pthread_create");
return -1;
}
pthread_detach(tid);
}
close(fd);
return 0;
}
void* clientThread(void* arg){
int newFd = (int)arg;
//5.数据交互
while(1){
write(newFd,"server",strlen("server\n"));
sleep(1);
}
printf("client exit\n");
close(newFd);
return NULL;
}
UDP
测试
1、使用ifconfig获取环回IP地址
2、使用 nc -u <IP地址> <端口号>使得终端作为客户端连接服务器(这里服务器端口号为8888)
单线程
UDP客户端与服务端实现步骤:
server.c代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc,char** argv){
int fd;
struct sockaddr_in addr;
//判断参数有效性
if(argc != 3){
printf("param err\n");
printf("%s<ip><port>\n",argv[0]);
return -1;
}
printf("ip = %s\n",argv[1]);
printf("port = %s\n",argv[2]);
//1.创建socket
if((fd=socket(AF_INET,SOCK_DGRAM,0))<0){//IPv4,UDP协议
perror("socket");
return -1;
}
//2.绑定IP、端口号
addr.sin_family = AF_INET; //IPv4
addr.sin_port = htons(atoi(argv[2])); //端口号,要转化为大端子节序
addr.sin_addr.s_addr = inet_addr(argv[1]); //IP地址:0表示在本网络上的本主机,即:自己
if(bind(fd,(struct sockaddr*)&addr,sizeof(struct sockaddr_in)) == -1){
perror("bind");
return -1;
}
//3.数据交互
char buf[100] = {0};
struct sockaddr_in src_addr;
socklen_t src_addrlen;
while(1){
memset(buf,0,sizeof(buf));
if(recvfrom(fd,buf,sizeof(buf)-1,0,(struct sockaddr*)&src_addr,&src_addrlen) > 0){
printf("client port = %d\n",ntohs(src_addr.sin_port));
printf("client ip = %s\n",inet_ntoa(src_addr.sin_addr));
printf("read:%s\n",buf);
}
}
close(fd);
return 0;
}
client.c代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc,char** argv){
int fd;
struct sockaddr_in addr;
//判断参数有效性
if(argc != 3){
printf("param err\n");
printf("%s<ip><port>\n",argv[0]);
return -1;
}
printf("ip = %s\n",argv[1]);
printf("port = %s\n",argv[2]);
//1.创建socket
if((fd=socket(AF_INET,SOCK_DGRAM,0))<0){//IPv4,UDP协议
perror("socket");
return -1;
}
//2.设置要发送到的服务器信息
addr.sin_family = AF_INET; //IPv4
addr.sin_port = htons(atoi(argv[2])); //服务器端口号,要转化为大端子节序
addr.sin_addr.s_addr = inet_addr(argv[1]); //服务器IP地址:在本网络上的本主机,即:自己
//3.数据交互
while(1){
sendto(fd,"cilent",strlen("cilent"),0,(struct sockaddr*)&addr,sizeof(addr));
sleep(1);
}
close(fd);
return 0;
}