一、主机字节序列和网络字节序列
(一)主机字节序
CPU在内存中进行数据存取的顺序。
二进制存在高位和低位之分:左边是高位,右边是低位;
内存存储存在高地址和低地址之分:一块空间起始处总是低地址(一个存储单元大于1字节的数据,就会存在字节序的问题)
-
大端字节序:一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处(高地址存低位,低地址存高位,例如:0x01020304 -> 01 02 03 04)
-
小端字节序:一个整数的高位字节存储在内存的高地址处,低位字节存储在内存的低地址处(高地址存高位,低地址存低位,例如:0x01020304 -> 04 03 02 01)
字节序的决定性:CPU架构
-
X86:小端字节序
-
MIPS:大端字节序
(二)网络字节序
在两台使用不同字节序的主机之间传递数据时,可能会出现冲突,所以,在将数据发送到网络时规定整形数据使用大端字节序,所以也把大端字节序称为网络字节序。对方接收到数据后,可以根据自己的字节序进行转换
(三)字节序转换接口
字节序转换针对的数据类型:short,int,long,float,double...(大于1字节的类型)
(1)从主机字节序转换为网络字节序
- 2字节数据的转换:uint16_t htons(uint16_t val);(常用于转换端口)
- 4字节数据的转换:uint32_t htonl(uint32_t val);(常用于转换IP地址)
注意:短整型数据的转换不能使用 htonl
(2)从网络字节序转换为主机字节序
- 2字节数据的转换:uint16_t ntohs(uint16_t val);(常用于转换端口)
- 4字节数据的转换:uint32_t ntohl(uint32_t val);(常用于转换IP地址)
(3)点分十进制的转换
IP地址本质就是一个4字节的无符号整数,用ntohl,htonl进行转换即可;但是,为了使IP地址能够更加便于记忆,会采用点分十进制字符串进行表示(例如:192.168.12.12);所以很多时候,IP地址的转换,并不是转换实际的数字,而是一个字符串。
- 将字符串点分十进制IPv4地址,转换为网络字节序数字IPv4地址
cpp
struct in_addr inet_addr(const char *ipstr);
- 将网络字节序数字IPv4地址,转换为字符串点分十进制IPv4地址
cpp
const char* inet_ntoa(sruct in_addr addr); // 非线程安全接口
- 将网络字节序整数IP地址转换为字符串IP地址(可转换IPv4或IPv6)
cpp
const char* inet_ntop(int domain, void *src, char *dst, size_t size);
/*
domain:描述要转换的地址类型,AF_INET(ipv4)或 AF_INET6(ipv6)
src:网络字节序的数据地址
dst:转换后的字符串存放空间地址
size:dst空间的大小
*/
- 将字符串的IP地址,转换为网络字节序IP地址(可转换为IPv4或IPv6)
cpp
int inet_pton(int domain, const char *src, void *dst);
二、套接字编程
(一)网络通信
网络通信也是进程间通信方式的一种,相较于以前的进程间通信,网络通信可以实现跨主机通信
网络通信也是进程间通信方式,但是通信两端有不同的角色:
- 客户端:主动向服务器发送请求,获取服务的一端,通常部署在客户机上
- 服务端:通常部署在网络服务提供商的服务器上,接收客户端请求并提供服务
通信分为两种不同的架构:
- C/S:客户端服务器模式(client/server)
- B/S:浏览器服务器模式(browser/server)
(二)套接字地址结构
套接字:一套网络通信API接口
1.通用 socket 地址结构
socket 网络编程接口中表示 socket 地址的是结构体 sockaddr
cpp
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
};
sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应,常见的协议族和对应的地址族如下:

2.专用 socket 地址结构
TCP/IP协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体,它们分别用于IPv4和IPv6
cpp
struct in_addr
{
u_int32_t s_addr;
};
struct sockaddr_in
{
sa_family_t sin_family; // 地址族 AF_INET
u_int16_t sin_port; // 端口号 用网络字节序表示
struct in_addr sin_addr; // IVP4地址结构 s_addr以网络字节序表示IPV4地址
};
struct in6_addr
{
unsigned char sa_addr[16]; //IPV6地址 用网络字节序表示
};
struct sockaddr_in6
{
sa_family_t sin6_family; // 地址族 AF_INET
u_inet16_t sin6_port; // 端口号 用网络字节序表示
u_int32_t sin6_flowinfo; // 流信息 应设置为0
struct in6_addr sin6_addr; // IVP6地址结构
u_int32_t sin6_scope_id; // scope ID
};
三、网络编程接口
(一)创建套接字
在内核中创建网络通信描述信息,并获取套接字的文件描述符
cpp
int socket (int domain, int type, int protocol);
/*
domain:地址域,描述通信地址类型
AF_UNIX:域间通信(本地通信),struct sockaddr_un
AF_INET:IPV4通信,struct sockaddr_in
AF_INET6:IPV6通信,struct sockaddr_in6
type:设置套接字的服务类型
SOCK_STREAM:流式套接字,有序的,可靠的,双向的,基于字节流
SOCK_DGRAM:数据报套接字,无连接的,不可靠的,有最大长度限制的
SOCK_RAW:提供更底层原始的数据包捕获
protocol:协议类型,选择要使用的协议
默认值 0:SOCK_STREAM是tcp协议;SOCK_DGRAM是udp协议
IPPROTO_TCP:TCP协议
IPPROTO_UDP:UDP协议
返回值:
成功,则返回套接字的文件描述符(非负整数);
失败,返回 -1
*/
(二)为套接字绑定地址信息
将sockfd与一个socket地址绑定
作用:告诉网卡,发送数据的时候,数据的源端地址信息是什么(当前设备网卡的IP地址+端口号);告知操作系统,网卡接收数据的时候,哪些数据,应该交给当前套接字进行处理
cpp
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
/*
sockfd:套接字描述符,socket返回的描述符
addr:要绑定的地址信息
addrlen:地址信息长度
返回值:成功返回 0;失败返回 -1
*/
(三)连接服务器(用于TCP协议客户端)
客户端通过此系统调用来主动与服务器建立连接
cpp
int connect(int sockfd,const struct sockaddr *serv_addr, socklen_t addrlen);
/*
serv_addr:要连接的服务端地址信息
addrlen:指定这个地址的长度
返回值:成功返回 0;失败返回 -1
*/
(四)开始监听(用于TCP协议服务端)
创建一个监听队列以存储待处理的客户连接
cpp
int listen(int sockfd,int backlog);
/*
sockfd:被监听的socket套接字
backlog:同一时间能够接受最大客户端连接请求数量
返回值:成功返回 0,失败返回 -1
*/
(五)获取就绪连接(用于TCP协议服务端)
从listen监听队列中接收一个连接,返回一个新的连接socket,该socket唯一地标识了被接收地这个连接
cpp
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*
sockfd:执行过listen系统调用的监听socket
addr:获取呗接受连接的远端socket地址
addrlen:既要设置要获取多长的地址,并且还要返回实际的地址信息长度
返回值:成功则返回指定客户端通信句柄描述符;失败返回 -1
*/
(六)接收数据
接收数据的接口是一个阻塞接口(若没有数据则等待)
1.TCP的接收接口
读取sockfd上的数据
cpp
ssize_t recv(int sockfd, void *buff, size_t len, int flags)
/*
sockfd:对于客户端来说就是socket的返回值;对于服务端来说是accept的返回值
buff:指定读缓冲区的位置
len:指定读缓冲区的大小
flags:为数据接收提供了额外的控制
通常默认为 0,表示阻塞接收
MSG_DONTWAIT:本次recv能拿到数据则直接返回,没有数据则报错返回
MSG_PEEK:接收数据,但是不从缓冲区删除数据,常用于检测性接收
返回值:成功则返回实际读取到的数据长度;出错则返回-1;TCP通信连接断开,返回0
*/
2.UDP的接收接口
读取sockfd上的数据
cpp
ssize_t recvfrom(int sockfd, void *buff, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)
/*
buff:指定读缓冲区的位置
len:指定读缓冲区的大小
src_addr:记录发送端的socket地址
addrlen:指定该地址的长度
返回值:成功则返回实际读取到的数据长度;出错则返回-1
*/
(七)发送数据
发送数据的接口是一个阻塞接口(发送过快,导致数据将socket发送缓冲区塞满了就会阻塞)
1.TCP的发送接口
往socket上写入数据
cpp
ssize_t send(int sockfd, const void *buff, size_t len, int flags);
/*
buff:指定写缓冲区的位置
len:指定写缓冲区的数据长度
flags:为数据发送提供了额外的控制
通常默认为0,表示缓冲区满的时候,函数阻塞,等待缓冲区腾出空间
返回值:实际发送的数据长度; 出错则返回-1;
*/
2.UDP的发送接口
往socket上写入数据
cpp
ssize_t sendto(int sockfd, void *buff, size_t len, int flags, struct sockaddr* dest_addr, socklen_t addrlen);
/*
buff:指定写缓冲区的位置
len:指定写缓冲区的数据长度
dest_addr:指定接收数据端的socket地址
addrlen:指定该地址的长度
返回值:实际发送的数据长度; 出错则返回-1
*/
(八)关闭套接字
关闭一个连接,实际上就是关闭该连接对应的socket
cpp
int close(int sockfd);
四、UDP编程流程
UDP提供的是无连接、不可靠的、数据报服务
(一)编程流程

-
sokcet()方法是用来创建一个套接字,有了套接字就可以通过网络进行数据的收发;创建套接字时要指定使用的服务类型,使用udp协议选择数据报服务SOKC_DGRAM
-
bind()方法是用来指定套接字使用的IP地址和端口
IP地址就是自己主机的地址,如果主机没有接入网络,测试程序时可以使用回环地址"127.0.0.1"。端口是一个16位的整形值,一般0-1024为知名端口,这类端口一般用户不能随便使用;1024-4096为保留端口,用户一般也不使用;4096以上为临时端口,用户可以使用;在Linux上,1024以内的端口号,只有root用户可以使用
-
sendto()用来发送数据,由于UDP是无连接的,每次发送数据都需要指定对端的地址(IP和端口)
-
recvfrom()接收数据,每次都需要传给该方法一个地址结构来存放发送端的地址;recvfrom()可以接收所有客户端发送给当前应用程序的数据,并不是只能接收某一个客户端的数据
-
close()关闭连接
(二)代码编写
1.服务端
cpp
/*
编写一个简单的UDP通信服务端程序:
功能:模拟简单的聊天程序,
1.接收客户端消息进行展示
2.从标准输入获取一个信息响应给客户端
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h> // close
#include <sys/socket.h> // socket api接口
#include <netinet/in.h> // 地址结构和协议宏声明
#include <arpa/inet.h> // 字节序转换的api接口
int main()
{
// 1.创建套接字
// int socket(int domain, int type, int protocol);
// AF_INET:IPv4通信;SOCK_DGRAM:数据报套接字
// 协议类型也可以给0表示UDP,因为SOCK_DGRAM数据报套接字默认协议就是UDP
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockfd < 0)
{
perror("创建套接字失败:");
return -1;
}
// 1.5 构造地址信息结构,填充要绑定的地址信息
// 绑定的IP地址必须是当前一有的网卡IP地址,绑定哪个网卡则接收的就是哪个网卡收到的数据
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9090); // 要转换为网络字节序
// in_addr_t inet_addr(const char *__cp)
// int inet_pton(int af, const char *src, void *dst);
// 方法一
// addr.sin_addr.s_addr = inet_addr("192.168.182.128");
// 方法二
inet_pton(AF_INET, "192.168.182.128", &addr.sin_addr);
// 2.为套接字绑定地址信息
// int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
socklen_t addrlen = sizeof(struct sockaddr_in);
int ret = bind(sockfd, (struct sockaddr *)&addr, addrlen);
if (ret < 0)
{
perror("绑定地址信息失败");
return -1;
}
while (1)
{
// 3.接受客户端数据
// ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
struct sockaddr_in peerAddr;
socklen_t addrlen = sizeof(struct sockaddr_in);
char buffer[1024] = {0}; // 用于存放获取出来的数据
ret = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr *)&peerAddr, &addrlen);
if (ret < 0)
{
perror("recvfrom接收数据失败");
continue;
}
printf("客户端[%s:%d] say:%s\n", inet_ntoa(peerAddr.sin_addr), ntohs(peerAddr.sin_port), buffer);
// 4.捕获键盘输入
memset(buffer, 0x00, 1024);
printf("服务器 say:");
fflush(stdout);
scanf("%s", buffer);
// 5.向客户端响应数据
// ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ret = sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peerAddr, addrlen);
if (ret < 0)
{
perror("sendto 发送数据失败");
continue;
}
}
// 6.关闭套接字
close(sockfd);
return 0;
}
2.客户端
cpp
/*
编写一个简单的UDP通信客户端程序:
功能:模拟一个简单的聊天功能
1.从标准输入捕获一个字符串,发送给服务端
2.等待服务端的响应,接收后展示出来
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h> // close
#include <sys/socket.h> // socket api接口
#include <netinet/in.h> // 地址结构和协议宏声明
#include <arpa/inet.h> // 字节序转换的api接口
#define CHECH_RESULT(ret, msg) \
if ((ret) < 0) \
{ \
perror(msg); \
return -1; \
}
int main()
{
// 1.创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
CHECH_RESULT(sockfd, "创建套接字失败");
// 2.为套接字绑定地址信息(大多时候不需要主动绑定)
// 2.5 构造地址信息结构
struct sockaddr_in srvAddr;
srvAddr.sin_family = AF_INET;
srvAddr.sin_port = htons(9090);
srvAddr.sin_addr.s_addr = inet_addr("192.168.182.128");
socklen_t addrlen = sizeof(struct sockaddr_in);
while (1)
{
// 3.捕获键盘输入
char buffer[1024] = {0};
printf("客户端 say:");
fflush(stdout);
scanf("%s", buffer);
// 4.发送数据
ssize_t ret = sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&srvAddr, addrlen);
CHECH_RESULT(ret, "发送数据失败");
// 5.接收服务端响应,进行展示
memset(buffer, 0x00, 1024);
ret = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr *)&srvAddr, &addrlen);
CHECH_RESULT(ret, "接收响应失败")
printf("服务端 say:%s\n", buffer);
}
// 6.关闭套接字
close(sockfd);
return 0;
}
五、TCP编程流程
(一)编写流程
TCP提供的是面向连接的、可靠的、字节流服务
- 面向连接:两端必须先建立连接,才能通信
- 可靠传输:确保数据能够安全、有序的到达对端
- 面向字节流:以字节为单位进行传输,不限制传输大小,先发先到
在TCP通信中,套接字是区分了两种:
- 监听套接字:是最早服务端通过socket创建的套接字,只用于处理新连接
- 通信套接字:是客户端连接到服务器成功后创建的新的套接字,用于与客户端通信

- sokcet()方法是用来创建一个套接字,有了套接字就可以通过网络进行数据的收发;创建套接字时要指定使用的服务类型,使用TCP协议选择流式服务SOCK_STREAM
- bind()方法是用来指定套接字使用的IP地址和端口
- connect()方法一般由客户端程序执行,用来与客户端建立连接
- listen方法时用来创建监听队列
- accept()用来从listen监听队列中接收一个连接,返回一个新的连接socket,该socket唯一地标识了被接收地这个连接
- send()方法用来向TCP连接的对端发送数据
- recv()方法用来接收TCP连接的对端发送来的数据
- close()关闭连接
(二)代码编写
1.服务端
cpp
/*
实现一个简单的tcp服务端程序:模拟简单聊天
1.接收客户端的消息,进行展示
2.从键盘捕获输入,发送给客户端
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h> // close
#include <sys/socket.h> // socket api
#include <netinet/in.h> // struct sockaddr_in
#include <arpa/inet.h> // 字节序转换htons
#define CHECK_RESULT(ret, msg) \
if ((ret) < 0) \
{ \
perror(msg); \
return -1; \
}
int main()
{
// 1.创建套接字
// int socket(int domain, int type, int protocol);
int listen_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
CHECK_RESULT(listen_fd, "创建套接字失败");
// 2.构造监听地址结构对象,为套接字绑定地址信息
// int bind(int listen_fd, const struct sockaddr *addr, socklen_t addrlen);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9000);
addr.sin_addr.s_addr = inet_addr("192.168.182.128");
socklen_t addrlen = sizeof(struct sockaddr_in);
int ret = bind(listen_fd, (struct sockaddr *)&addr, addrlen);
CHECK_RESULT(ret, "绑定地址失败");
// 3.开始监听
// int listen(int listen_fd, int backlog);
ret = listen(listen_fd, 1024);
CHECK_RESULT(ret, "监听失败");
while (1)
{
// 4.获取新连接(与客户端的通信都是通过获取的新连接进行的)
// int accept(int listen_fd, struct sockaddr *addr, socklen_t *addrlen);
// insten_fd:监听套接字描述符
// *addr:客户端地址信息
// *addrlen:指定想要获取的地址长度,以及返回实际获取的地址长度
// 返回值:新建连接的套接字描述符(用于往后的数据通信)
struct sockaddr_in peerAddr;
int newfd = accept(listen_fd, (struct sockaddr *)&peerAddr, &addrlen);
if (newfd < 0)
{
perror("获取新连接出错");
continue;
}
char peer_ip[24] = {0};
inet_ntop(AF_INET, &peerAddr.sin_addr, peer_ip, 23);
printf("新的客户端连接建立成功:[%s:%d]\n", peer_ip, ntohs(peerAddr.sin_port));
while (1)
{
// 5.接收客户端消息,进行展示
// ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 返回值:成功则返回世纪接收的数据长度
// 失败出错,则返回-1
// 若返回0,则表示连接断开
char buffer[1024] = {0};
ret = recv(newfd, buffer, 1023, 0);
if (ret < 0)
{
perror("接收数据出错");
continue;
}
else if (ret == 0)
{
printf("与客户端连接断开\n");
close(newfd);
break;
}
printf("客户端say:%s\n", buffer);
// 6.捕获键盘输入,向客户端发送数据
// ssize_t send(int sockfd, const void *buf, size_t len, int flags);
memset(buffer, 0x00, 1024);
printf("输入数据:");
fflush(stdout);
scanf("%s", buffer);
ret = send(newfd, buffer, strlen(buffer), 0);
if (ret != strlen(buffer))
{
if (ret < 0)
{
perror("发送数据出错");
close(newfd);
continue;
}
printf("数据发送不完整");
}
}
}
// 7.关闭套接字
close(listen_fd);
return 0;
}
2.客户端
cpp
/*
实现一个简单的tcp服务端程序:模拟简单聊天
1.从键盘捕获数据,发送给服务器
2.等待服务器的响应,进行展示
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h> // close
#include <sys/socket.h> // socket api
#include <netinet/in.h> // struct sockaddr_in
#include <arpa/inet.h> // 字节序转换htons
#define CHECK_RESULT(ret, msg) \
if ((ret) < 0) \
{ \
perror(msg); \
return -1; \
}
int main()
{
// 1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
CHECK_RESULT(sockfd, "创建套接字失败");
// 2.绑定地址信息(不主动)
// 3.向服务器发送连接请求
// int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9000);
addr.sin_addr.s_addr = inet_addr("192.168.182.128");
socklen_t addrlen = sizeof(struct sockaddr_in);
int ret = connect(sockfd, (struct sockaddr *)&addr, addrlen);
CHECK_RESULT(ret, "连接服务器失败");
while (1)
{
// 4.捕获键盘数据,向服务器发送数据
// ssize_t send(int sockfd, const void *buf, size_t len, int flags);
char buffer[1024] = {0};
memset(buffer, 0x00, 1024);
printf("输入数据:");
fflush(stdout);
scanf("%s", buffer);
ret = send(sockfd, buffer, strlen(buffer), 0);
if (ret < 0)
{
perror("发送数据失败");
close(sockfd);
return -1;
}
// 5.等待服务端响应,展示响应数据
// ssize_t recv(int sockfd, void *buf, size_t len, int flags);
memset(buffer, 0x00, 1024);
ret = recv(sockfd, buffer, 1023, 0);
if (ret <= 0)
{
perror("接收数据失败");
close(sockfd);
return -1;
}
printf("服务端 say:%s\n", buffer);
}
// 6.关闭套接字
close(sockfd);
return 0;
}
六、多进程、多线程处理并发
(一)并发原理
tcp服务端存在的问题:
在socket套接字API中,accept获取新连接,recv接收数据,send发送数据,都是默认阻塞接口(功能未完成就会一直等待);这些阻塞接口导致,当前在单执行流中,无法正常的与每个客户端持续进行通信(要么只能与一个客户端持续通信;要么就只能与一个客户端通信一次)
解决方案:将每一个阻塞操作都放到一个单独的执行流中
多执行流有两种实现方案:
- 方案一:多进程:更加稳定,健壮性更强
- 方案二:多线程:成本更低,线程间通信更加方便
实现一个并发服务器(可以同时与多个客户端进行通信的服务器):服务端接受一个客户端的连接后,创建一个线程或者进程,然后在新创建的线程或进程中循环处理数据;主线程(父进程)只负责监听客户端的连接,并使用 accept() 接受连接,不进行数据的处理。

多执行流版本的并发服务器,并不是一个好的解决方案:资源有限
需要为每一个客户端连接都创建一个执行流
执行流并不是越多越好(多了就会增加cou时间片切换成本)
(二)代码编写
1.多进程(服务端)
cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h> // close
#include <sys/socket.h> // socket api
#include <netinet/in.h> // struct sockaddr_in
#include <arpa/inet.h> // 字节序转换htons
#include <signal.h>
#define CHECK_RESULT(ret, msg) \
if ((ret) < 0) \
{ \
perror(msg); \
return -1; \
}
// 实现一个多进程版本的服务器:创建进程,在子进程中与客户端进行通信
void worker(int sockfd)
{
pid_t pid = fork();
if (pid < 0)
{
perror("创建子进程失败");
return;
}
if (pid > 0)
{
return; // 父进程直接返回,回到main
}
while (1)
{
char buffer[1024] = {0};
int ret = recv(sockfd, buffer, 1023, 0);
if (ret < 0)
{
perror("接收数据出错");
continue;
}
else if (ret == 0)
{
printf("与客户端连接断开\n");
break;
}
printf("客户端say: %s\n", buffer);
// 6.捕获键盘输入,向客户端发送数据
// ssize_t send(int sockfd, const void *buf, size_t len, int flags);
memset(buffer, 0x00, 1024);
printf("输入数据:");
fflush(stdout);
scanf("%s", buffer);
ret = send(sockfd, buffer, strlen(buffer), 0);
if (ret != strlen(buffer))
{
if (ret < 0)
{
perror("发送数据出错");
close(sockfd);
continue;
}
printf("数据发送不完整\n");
}
}
close(sockfd);
exit(0); // 当前在子进程中,这里退出的是子进程
}
int main()
{
//主动忽略SIGCHLD信号:告诉系统我不关注子进程状态
//显式的告诉系统,子进程退出后,直接释放所有资源
signal(SIGCHLD, SIG_IGN);
// 1.创建套接字
// int socket(int domain, int type, int protocol);
int listen_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
CHECK_RESULT(listen_fd, "创建套接字失败");
// 2.构造监听地址结构对象,为套接字绑定地址信息
// int bind(int listen_fd, const struct sockaddr *addr, socklen_t addrlen);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9000);
addr.sin_addr.s_addr = inet_addr("192.168.182.128");
socklen_t addrlen = sizeof(struct sockaddr_in);
int ret = bind(listen_fd, (struct sockaddr *)&addr, addrlen);
CHECK_RESULT(ret, "绑定地址失败");
// 3. 开始监听
ret = listen(listen_fd, 1024);
CHECK_RESULT(ret, "监听失败");
while (1)
{
struct sockaddr_in peerAddr;
int newfd = accept(listen_fd, (struct sockaddr *)&peerAddr, &addrlen);
if (newfd < 0)
{
perror("获取新链接出错");
continue;
}
char peer_ip[24] = {0};
inet_ntop(AF_INET, &peerAddr.sin_addr, peer_ip, 23);
printf("新的客户端连接建立成功: [%s:%d]\n", peer_ip, ntohs(peerAddr.sin_port));
worker(newfd);
close(newfd); //父进程中并没有用到这个描述符
}
return 0;
}
多进程注意事项:
通过信号处理,避免产生僵尸进程
父进程中创建子进程之后,要关闭新建套接字(父进程没用到)
2.多线程(服务端)
cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h> // close
#include <sys/socket.h> // socket api
#include <netinet/in.h> // struct sockaddr_in
#include <arpa/inet.h> // 字节序转换htons
#include <pthread.h>
#define CHECK_RESULT(ret, msg) \
if ((ret) < 0) \
{ \
perror(msg); \
return -1; \
}
void *entry(void *arg)
{
int sockfd = (int)arg;
while (1)
{
// 5.接收客户端消息,进行展示
// ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 返回值:成功则返回世纪接收的数据长度
// 失败出错,则返回-1
// 若返回0,则表示连接断开
char buffer[1024] = {0};
int ret = recv(sockfd, buffer, 1023, 0);
if (ret < 0)
{
perror("接收数据出错");
continue;
}
else if (ret == 0)
{
printf("与客户端连接断开\n");
break;
}
printf("客户端say:%s\n", buffer);
// 6.捕获键盘输入,向客户端发送数据
// ssize_t send(int sockfd, const void *buf, size_t len, int flags);
memset(buffer, 0x00, 1024);
printf("输入数据:");
fflush(stdout);
scanf("%s", buffer);
ret = send(sockfd, buffer, strlen(buffer), 0);
if (ret != strlen(buffer))
{
if (ret < 0)
{
perror("发送数据出错");
close(sockfd);
continue;
}
printf("数据发送不完整");
}
}
close(sockfd);
}
// 创建一个线程,负责通过当前的描述符与客户端进行通信
void worker(int sockfd)
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, entry, (void *)sockfd);
if (ret != 0)
{
perror("创建工作线程失败");
return;
}
pthread_detach(tid); // 分离线程,让线程退出后自动释放所有资源,不需要被等待
}
int main()
{
// 主动忽略SIGCHLD信号------告诉系统我不关注子进程状态
// 显示的告诉系统,子进程退出后,直接释放所有资源
signal(SIGCHLD, SIG_IGN);
// 1.创建套接字
// int socket(int domain, int type, int protocol);
int listen_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
CHECK_RESULT(listen_fd, "创建套接字失败");
// 2.构造监听地址结构对象,为套接字绑定地址信息
// int bind(int listen_fd, const struct sockaddr *addr, socklen_t addrlen);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9000);
addr.sin_addr.s_addr = inet_addr("192.168.182.128");
socklen_t addrlen = sizeof(struct sockaddr_in);
int ret = bind(listen_fd, (struct sockaddr *)&addr, addrlen);
CHECK_RESULT(ret, "绑定地址失败");
// 3.开始监听
// int listen(int listen_fd, int backlog);
ret = listen(listen_fd, 1024);
CHECK_RESULT(ret, "监听失败");
while (1)
{
// 4.获取新连接(与客户端的通信都是通过获取的新连接进行的)
// int accept(int listen_fd, struct sockaddr *addr, socklen_t *addrlen);
// insten_fd:监听套接字描述符
// *addr:客户端地址信息
// *addrlen:指定想要获取的地址长度,以及返回实际获取的地址长度
// 返回值:新建连接的套接字描述符(用于往后的数据通信)
struct sockaddr_in peerAddr;
int newfd = accept(listen_fd, (struct sockaddr *)&peerAddr, &addrlen);
if (newfd < 0)
{
perror("获取新连接出错");
continue;
}
char peer_ip[24] = {0};
inet_ntop(AF_INET, &peerAddr.sin_addr, peer_ip, 23);
printf("新的客户端连接建立成功:[%s:%d]\n", peer_ip, ntohs(peerAddr.sin_port));
worker(newfd);
// close(newfd); // 线程间资源共享,这里如果关闭描述符会导致普通线程无法使用
}
// 7.关闭套接字
close(listen_fd);
return 0;
}
多线程注意事项:
创建线程后,要分离线程(不需要被等待了)
主线程虽然不用newfd,但是也不能关闭(线程间共享进程资源)