文章目录
- TCP IP网络编程
- 一、基础知识(TCP)
- 1)Linux
- 1. socket()
- 2.bind()
- 2.1前提
- 2.2字节序与网络字节序
- 2.3 字节序转换
- 2.4 字符串信息转化成网络字节序的整数型
- 2.5 INADDR_ANY
- 3.listen()
- 4.accept()
- 5.connect()
- 6.案例小结
- 6.1服务器端
- 6.2 客户端
- 7.半关闭
- 2)Window
- 1. socket()
- 2.bind()
- 3.listen()
- 4.accept()
- 5.recv()
- 6.send()
- 二、UDP基础知识
- 1)Linux
- 1.sendto()
- 2.recvfrom()
- 3.已连接UDP套接字
- 4.案例
- 2)Window
- 三、域名
- 1.由域名得到ip
- 2.通过IP地址获取域名
- 3.Window
- 四、套接字的多种选项
- 1.获取和设置套接字选项
- 2.各种选项
- 3.Nagle算法
- 五、多进程服务端
- 1.案例
- 2.分割I/O
- 六、进程间通信
- 1.基于管道(PIPE)的通信
- 七、并发服务器
- 1.I/O复用
- 1.1 select()
- 1.2.epoll()
- 1.2.2 案例
- 1.2.3边沿触发与条件触发
- 2.多进程实现并发
- 3.多线程服务器端
- 3.1 创建线程
- 3.2 等待线程的消亡
- 3. 3 互斥量
- 3.4 信号量
- 八、多种I/O函数
- 1.send(),recv()
- 2.readv(),writev()
- 九、多播和广播
- 1.设置TTL
- 2.加入多播组
- 3.广播
- 十、Linux编程
- 1.标准IO与系统IO之间的相互转化
- 2.复制文件描述符dup
- 3.实现半关闭
TCP IP网络编程
一、基础知识(TCP)
1)Linux
1. socket()
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/*
domain: 套接字中使用的协议族信息
type : 套接字数据传输类型信息
protocol :计算机间通信中使用的协议信息。【最终决定采用什么协议】
*/
int socket(int domain, int type, int protocol);
domain
名称 | 描述 |
---|---|
PF_INET | IPv4互联网协议族 |
PF_INET6 | IPv6互联网协议族 |
PF_LOCAL | 本地通信的UNIX协议族 |
PF_PACKET | 底层套接字的协议族 |
PF_IPX | IPX Novell协议族 |
type
- 面向连接的套接字(SOCK_STREAM) 【跟TCP一样】
特点:可靠的,按序传递的,基于字节的面向连接的数据传输方式的套接字。
- 面向消息的套接字(SOCK_DGRAM) 【跟UDP一样】
特点:不可靠的,不按序传递的,以数据的高速传输为目的的套接字。
protocol
传递前两个参数即可创建所需套接字,第三个参数是为了以下情况:
同一协议族中存在多个数据传输方式相同的的协议。
这时需要通过第三个参数具体指定协议信息。
IPv4协议族中面向连接的套接字,协议只有IPPROTO_TCP。
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
这个套接字称为TCP套接字。
IPv4协议族中面向消息的套接字,协议只有IPPROTO_UDP
int udp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_UDP);
这个套接字称为UDP套接字。
2.bind()
2.1前提
struct sockaddr_in
{
sa_family_t sin_family; //地址族
uint16_t sin_port; //16位TCP/UDP端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8];//不使用
}
struct in_addr
{
In_addr_t s_addr; //32位IPv4地址
}
sin_family
地址族 | 含义 |
---|---|
AF_INET | IPv4网络协议中使用的地址族 |
AF_INET6 | IPv6网络协议中使用的地址族 |
AF_LOCAL | 本地通信中采用的UNIX协议的地址族 |
sin_port
它以网络字节序保存。
sin_addr
以网络字节序保存。
sin_zero
无特殊含义。只是为了使结构体sockaddr_in的大小与sockaddr结构体保持一致而插入的成员。必须填充为0,否则无法得到想要的结果。
struct sockaddr
{
sa_family_t sa_family; //地址族
char sa_data[14]; //地址信息
}
此结构体成员sa_data保存的地址信息中需包含IP地址和端口号,剩余部分应填充0,这也是bind函数要求的。而这对于包含地址信息来讲非常麻烦,继而就有了新的结构体sockaddr_in。若按照之前的讲解填写sockaddr_in结构体,则将生成符合bind函数要求的字节流。最后转换为sockaddr型的结构体变量,再传递给bind函数即可。
2.2字节序与网络字节序
CPU向内存保存数据的方式有2种
- 大端序:高位字节存放到低地址。
- 小端序:高位字节存放到高位地址。
因为这种情况,所以网络传输时规定了统一的字节序。大端序,这就叫做网络字节序。
传输数据时,先把数据数组转化成大端序格式再进行网络传输。
2.3 字节序转换
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
解释:
- htons中的h代表主机(host)字节序。
- htons中的n代表网络(network)字节序。
- s指的是short
- l值的是long
htons的含义:把short型数据从主机字节序转化为网络字节序。
ntohs的含义:把short型数据从网络字节序转化成主机字节序。
通常,以s作为后缀的函数中,s代表2个字节shont,因此用于端口号转换;以1作为后缀的函数中,1代表4个字节,因此用于IP地址转换。
注意:除了向sockaddr in结构体变量填充数据外,其他情况无需考虑字节序问题。
2.4 字符串信息转化成网络字节序的整数型
#include <arpa/inet.h>
int_addr_t inet_addr(const char* string);
/* 把点分十进制ip地址,转化为32位整型数值,并按照网络字节序*/
成功返回32为整型数值,失败返回INADDR_NONE
同样功能的函数还有:
#include <arpa/inet.h>
int inet_aton(const char* string, struct in_addr* addr);
实际编程中若要调用inet_addr函数,需将转换后的IP地址信息代入sockaddr_in结构体中声明的in_addr结构体变量。而inet_aton函数则不需此过程。原因在于,若传递in_addr结构体变量地址值,函数会自动把结果填人该结构体变量。
char* addr = "127.232.124.79";
struct sockaddr_in addr_inet;
inet_acton(addr,&addr_inet.sin_addr);
将32为数值转化为字符串
#include <arpa/inet.h>
char* inet_ntoa(struct in_addr adr);
注意!!!
该函数将通过参数传入的整数型IP地址转换为字符串格式并返回。但调用时需小心,返回值类型为char指针。返回字符串地址意味着字符串已保存到内存空间,但该函数未向程序员要求分配内存,而是在内部申请了内存并保存了字符串。也就是说,调用完该函数后,应立即将字符串信息复制到其他内存空间。因为,若再次调用inet_ntoa函数,则有可能覆盖之前保存的字符串信息。总之,再次调用inet_ntoa函数前返回的字符串地址值是有效的。若需要长期保存,则应将字符串复制到其他内存空间。
2.5 INADDR_ANY
每次创建服务器端套接字都要输入IP地址会有些繁琐,所以采用常数INADDR_ANY分配服务器端的IP地址,可自动获取运行服务器端的计算机IP地址,不必亲自输入。
addr.sin_addr.s_addr = htonl(INADDR_ANY);
2.6 bind()
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/*
*/
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
成功返回0,失败返回-1
3.listen()
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/*
sockfd : 希望进入等待连接请求状态的套接字文件描述符
backlog:连接请求等待队列的长度。
*/
int listen(int sockfd, int backlog);
4.accept()
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/*
sock:服务器套接字的文件描述符
addr:保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息。
addrlen:第二个参数addr结构体的长度,但是存有长度的变量地址。函数调用完成后,该变量即被填入客户端地址长度。
*/
int accept(int sockfd, struct sockaddr *addr,
socklen_t *addrlen);
5.connect()
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/*
sockfd:客户端套接字文件描述符
addr:保存目标服务器端地址信息的变量地址值
addrlen:以字节为单位传递地址变量长度
*/
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
注意
客户端的IP地址和端口在调用connect函数时自动分配,无需调用bind函数进行分配。IP用计算机的IP,端口随机。
6.案例小结
6.1服务器端
#include <cstdio>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstring>
#include <arpa/inet.h>
void error_handling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char** argv)
{
if(argc != 2)
{
printf("Usage : %s <port> \n",argv[1]);
exit(1);
}
// 创建套接字
int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock < 0)
{
error_handling("serv_sock() error");
}
struct sockaddr_in serv_addr;
// 初始化地址
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
// 分配地址
int ret = bind(serv_sock,(struct sockaddr*)&serv_addr, sizeof(serv_addr));
if(ret < 0)
{
error_handling("bind() error");
}
// 开始监听
int ret1 = listen(serv_sock, 5);
if(ret1 < 0)
{
error_handling("listen() error");
}
int client_sock;
struct sockaddr_in client_addr;
socklen_t client_addr_size = sizeof(client_addr);
// 接受访问
client_sock = accept(serv_sock, (struct sockaddr*) &client_addr, &client_addr_size);
if(client_sock < 0)
{
error_handling("accept() error");
}
char message[] = "hello,world!";
// 传输数据
write(client_sock, message, sizeof(message));
// 传输完成,关闭套接字
close(client_sock);
close(serv_sock);
return 0;
}
6.2 客户端
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
void error_handling(const char* message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
int main(int argc, char** argv)
{
if(argc != 3)
{
printf("Usage : %s <ip> <port> \n", argv[0]);
exit(1);
}
int sock(-1);
// 创建套接字
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
perror("sock()");
// error_handling("socket() error");
}
// 初始化地址和端口号
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
// 发送请求
int ret = connect(sock, (struct sockaddr*)&serv_addr,sizeof(serv_addr));
if(ret < 0)
{
perror("connect()");
// error_handling("connect() error");
exit(1);
}
char message[30];
// 接收数据
int str_len = read(sock,message,sizeof(message)-1);
if(str_len < 0)
{
perror("read()");
// error_handling("read() error");
}
printf("Message from server : %s \n", message);
// 关闭套接字
close(sock);
return 0;
}
7.半关闭
Linux的close函数意味着完全断开连接。完全断开不仅指无法传输数据,而且也不能接收数据。
开一部分连接是指,可以传输数据但无法接收,或可以接收数据但无法传输。顾名思义就是只关闭流的一半。
#include <sys/socket.h>
int shutdown(int sockfd, int how);
how
名称 | 含义 |
---|---|
SHUT_RD | 断开输入流 |
SHUT_WR | 断开输出流 |
SHUT_RDWR | 同时断开I/O流 |
2)Window
1. socket()
#include <winsock2.h>
SOCKET socket(int af, int type, int protocol);
这些参数与Linux一样,只是返回值不同。SOCKET其实就是整数类型,微软把他重定义了。
出现错误时,返回INVALID_SOCKET。
#include <winsock2.h>
SOCKET soc = socket(PF_INET, SOCK_STREAM,IPPROTO_TCP);
if(soc == INVALID_SOCKET)
errorHandling("........");
2.bind()
与Linux完全相同。但是,Windows中不存在inet aton函数。
3.listen()
同Linux
4.accept()
同Linux
5.recv()
同linux的read()
6.send()
同Linux的write()
二、UDP基础知识
1)Linux
1.sendto()
#include <sys/types.h>
#include <sys/socket.h>
/*
sockfd : 用于传输数据的UDP套接字文件描述符
buff : 保存待传输数据的换成地址值
nbytes :待传输的数据长度,以字节为单位
flags :可选参数,若没有传递0
to : 存有目标地址信息的sockaddr结构体变量的地址值
addrlen :传递给参数to的地址值结构体变量长度
*/
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
成功返回传输的字节数,失败返回-1.
调用该函数时自动分配IP和端口号
2.recvfrom()
#include <sys/types.h>
#include <sys/socket.h>
/*
sockfd : 用于接收数据的UDP套接字文件描述符
buff : 保存接收数据的缓冲地址值
nbytes : 可接收的最大字节数,故无法超过参数buff所指的缓冲大小
flags : 可选参数,若没有则传入0
src_addr : 存有发送端地址信息的地址值
addrlen :保存参数from的结构体变量长度的变量地址值
*/
ssize_t recvfrom(int sockfd, void *buf, size_t len,
int flags,
struct sockaddr *src_addr,
socklen_t *addrlen);
3.已连接UDP套接字
如果对于某个目标,需要一直发送消息,那么就可以使用已连接的UDP。
和TCP套接字一样,使用connect函数,使用这个函数之后,就可以使用write和read函数。
4.案例
服务器端
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
const int BUF_SIZE = 30;
int main(int argc, char** argv)
{
// 判断
if(argc != 2)
{
fprintf(stderr,"Usage : %s <port> \n",argv[0]);
exit(1);
}
int serv_sock(-1);
// 创建套接字
serv_sock = socket(PF_INET,SOCK_DGRAM,0);
if(serv_sock < 0)
{
perror("socket()");
exit(1);
}
// 初始化IP地址和端口号
struct sockaddr_in sock_addr;
memset(&sock_addr, 0, sizeof(sock_addr));
sock_addr.sin_family = AF_INET; //地址族
sock_addr.sin_addr.s_addr = htonl(INADDR_ANY);//ip地址自动获取本机ip
sock_addr.sin_port = htons(atoi(argv[1]));
// 分配ip和端口号
int ret = bind(serv_sock, (struct sockaddr*)&sock_addr, sizeof(sock_addr));
if(ret < 0)
{
perror("bind()");
exit(1);
}
// 初始化客户请求
int client_sock(-1);
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// 保存客户端发来的信息
char message[BUF_SIZE];
int str_len(0);
while(true)
{
// 接收数据
str_len = recvfrom(serv_sock, message, BUF_SIZE, 0,
(struct sockaddr*)&client_addr,
&client_addr_len);
// 发送数据
sendto(serv_sock, message, str_len, 0,
(struct sockaddr*)&client_addr, client_addr_len);
}
close(serv_sock);
return 0;
}
客户端
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
const int BUF_SIZE = 30;
int main(int argc, char** argv)
{
if(argc != 3)
{
printf("Usage : %s <ip> <port> \n", argv[0]);
exit(1);
}
// 创建套接字
int clt_sock(-1);
clt_sock = socket(PF_INET, SOCK_DGRAM, 0);
if(clt_sock < 0)
{
perror("socket()");
exit(1);
}
// 初始化服务器地址
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
char message[BUF_SIZE];
struct sockaddr_in from_addr;
memset(&from_addr, 0, sizeof(from_addr));
socklen_t from_addr_len = sizeof(from_addr);
while (true)
{
fputs("Input message(Q to quit): ",stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message,"q\n") || !strcmp(message, "Q\n"))
break;
// write(clt_sock, message, strlen(message));
// 发送数据
sendto(clt_sock, message, strlen(message), 0,
(struct sockaddr*)&serv_addr, sizeof(serv_addr));
// int str_len = read(clt_sock, message, BUF_SIZE -1);
// message[str_len] = 0;
// 接收数据
int len = recvfrom(clt_sock, message, BUF_SIZE, 0,
(struct sockaddr*)&from_addr, &from_addr_len);
message[len] = 0;
printf("Message from server : %s", message);
}
close(clt_sock);
return 0;
}
2)Window
同Linux一模一样。
三、域名
1.由域名得到ip
#include <netdb.h>
extern int h_errno;
/* 通过域名获取ip地址*/
struct hostent *gethostbyname(const char *name);
struct hostent
{
char *h_name; /* official name of host */
char **h_aliases; /* alias list */
int h_addrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses */
}
成功返回结构体类型,失败返回NULL
- h_name :该变量中存有官方域名
- h_aliases :可以通过多个域名访问同一个主页。
- h_addrtype :保存地址族信息
- h_length : 保存IP地址长度
- h_addr_list : 保存域名1对应的IP地址
2.通过IP地址获取域名
#include <sys/socket.h> /* for AF_INET */
#include <netdb.h>
struct hostent *gethostbyaddr(const void *addr,
socklen_t len,
int type);
- addr : 含有IP地址信息的in_addr结构体的指针。
- len :向第一个参数传递的地址信息的字节数,IPv4时为4,IPv6时为16
- type:传递地址族信息,IPv4时为AF_INET,IPv6时为IP_INET6
3.Window
同Linux一样,连函数名和参数都相同。
四、套接字的多种选项
1.获取和设置套接字选项
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
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);
- sockfd :用于查看选项套接字文件描述符
- Level :要查看的可选项的协议层
- optname :要查看的可选项名
- optval :保存查看结果的缓冲地址值
- optlen :向第四个参数optval传递的缓冲大小。调用函数后,该变量中保存通过第四个参数返回的可选项信息的字节数。
2.各种选项
直接man函数,查看选项。或者上网搜索。
3.Nagle算法
为防止因数据包多过而发生网络过载。
五、多进程服务端
1.案例
服务器端
/**
* 多进程并发服务器
*/
#include <cstdio>
#include <cstdlib>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <cstring>
const int BUF_SIZE = 30;
void read_childproc(int sig)//声明信号处理函数
{
pid_t pid= waitpid(-1, NULL, WNOHANG);//给子进程收尸
printf("remove proc id : %d\n", pid);
}
int main(int argc, char** argv)
{
if(argc != 2)
{
fprintf(stderr,"Usage : %s <port>\n",argv[0]);
return 1;
}
// 设置子进程结束信号
struct sigaction act;
act.sa_handler = read_childproc;//设置信号处理函数
sigemptyset(&act.sa_mask);//置0
act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL);//设置信号
// 创建socket
int serv_sock(-1);
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock < 0)
{
perror("socket()");
return 1;
}
// 初始化ip,port
struct sockaddr_in serv_add;
memset(&serv_add, 0, sizeof(serv_add));
serv_add.sin_family = AF_INET;//IPv4协议族
serv_add.sin_addr.s_addr = htonl(INADDR_ANY);//自动获取本机ip
serv_add.sin_port = htons(atoi(argv[1]));
//分配ip,port
int ret = bind(serv_sock, (struct sockaddr*)&serv_add, sizeof(serv_add));
if(ret < 0)
{
perror("bind()");
return 1;
}
//进入请求等待
int ret0 = listen(serv_sock,5);
if(ret0 < 0)
{
perror("listen()");
return 1;
}
int clnt_sock(-1);//用来保存客户端套接字
struct sockaddr_in clnt_addr;//用来保存客户端地址
socklen_t clnt_size = sizeof(clnt_addr);
while(true)
{
//接收客户端请求
clnt_sock = accept(serv_sock,(struct sockaddr*)&clnt_addr, &clnt_size);
if(clnt_sock < 0)
{
continue;
}
else
puts("new client connection....");
pid_t pid;
pid = fork();//创建子进程
if(pid < 0)//error
{
close(clnt_sock);
continue;
}
else if(pid ==0)//child
{
close(serv_sock);//
int str_len(0);
char message[BUF_SIZE];
while((str_len = read(clnt_sock,message,BUF_SIZE)) != 0)
{
write(clnt_sock, message, str_len);
}
close(clnt_sock);
puts("client disconnected....");
return 0;
}
else //父进程
{
close(clnt_sock);//从队列中删除
}
}
close(serv_sock);
return 0;
}
客户端
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
const int BUF_SIZE = 1024;
int main(int argc, char** argv)
{
if(argc != 3)
{
printf("Usage : %s <ip> <port> \n", argv[0]);
exit(1);
}
// 创建套接字
int clt_sock(-1);
clt_sock = socket(PF_INET, SOCK_STREAM, 0);
if(clt_sock < 0)
{
perror("socket()");
exit(1);
}
// 初始化服务器地址
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
// 发送请求
int ret = connect(clt_sock,(struct sockaddr*)&serv_addr, sizeof(serv_addr));
if(ret < 0)
{
perror("connect()");
exit(1);
}
else
{
puts("Connected.......");
}
char message[BUF_SIZE];
while (true)
{
fputs("Input message(Q to quit): ",stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message,"q\n") || !strcmp(message, "Q\n"))
break;
write(clt_sock, message, strlen(message));
int str_len = read(clt_sock, message, BUF_SIZE -1);
message[str_len] = 0;
printf("Message from server : %s", message);
}
close(clt_sock);
return 0;
}
2.分割I/O
服务器端如1一样。
客户端
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
const int BUF_SIZE = 30;
void read_routine(int sock, char* buf)
{
while(true)
{
int str_len = read(sock, buf, BUF_SIZE);
if(str_len == 0)
return ;
buf[str_len] = 0;
printf("<Message from server> : %s", buf);
}
}
void write_routine(int sock, char* buf)
{
while(1)
{
fgets(buf, BUF_SIZE, stdin);
if(!strcmp(buf, "q\n") || !strcmp(buf,"Q\n"))
{
shutdown(sock, SHUT_WR);//关闭输出流,但是不关闭输入流
return;
}
write(sock, buf, strlen(buf));
}
}
int main(int argc, char** argv)
{
if(argc != 3)
{
printf("Usage : %s <ip> <port> \n", argv[0]);
exit(1);
}
// 创建套接字
int clt_sock(-1);
clt_sock = socket(PF_INET, SOCK_STREAM, 0);
if(clt_sock < 0)
{
perror("socket()");
exit(1);
}
// 初始化服务器地址
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
// 发送请求
int ret = connect(clt_sock,(struct sockaddr*)&serv_addr, sizeof(serv_addr));
if(ret < 0)
{
perror("connect()");
exit(1);
}
else
{
puts("Connected.......");
}
char message[BUF_SIZE];
// 创建子进程
pid_t pid;
pid = fork();
if(pid < 0)// error
{
perror("fork()");
return 1;
}
else if (pid == 0) //child
{
write_routine(clt_sock, message);
}
else
{
read_routine(clt_sock, message);
}
close(clt_sock);
return 0;
}
六、进程间通信
1.基于管道(PIPE)的通信
#include <unistd.h>
int pipe(int pipefd[2]);
成功返回0,失败返回-1.
- pipefd[0] : 通过管道接收数据时使用的文件描述符,即管道出口。
- pipefd[1] :通过管道传输数据时使用的文件描述,即管道入口。
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <string.h>
#include <wait.h>
const int BUF_SIZE = 40;
int main()
{
int fd[2];
int ret = pipe(fd);
if(ret < 0)
{
perror("pipe()");
return 1;
}
pid_t pid;
char message[BUF_SIZE];
pid = fork();
if(pid < 0)
{
perror("fork()");
return 1;
}
else if(pid == 0)
{
// sleep(4);
int str_len = read(fd[0],message,BUF_SIZE);
message[str_len] = 0;
printf("child read : %s\n", message);
close(fd[0]);
close(fd[1]);
}
else
{
// printf("in :\n");
// fputs(message, stdin);
char msg[] = "who are you ?";
write(fd[1], msg, strlen(msg));
close(fd[0]);
close(fd[1]);
wait(NULL);
}
return 0;
}
七、并发服务器
1.I/O复用
1.1 select()
该函数,移植性比较好。
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
struct timeval
{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
1.2.epoll()
这个函数仅仅适用于Linux,
linux的方言,不可以移植。
#include <sys/epoll.h>
/*
创建一个epoll描述符。
size 可以随便填,给个正数即可
*/
int epoll_create(int size);
int epoll_create1(int flags);
epoll_ctl()可以向指定的 epoll 上下文中加入或删除文件描述符:
#include <sys/epoll.h>
/* 管理一个epoll描述符 */
int epoll_ctl(int epfd, int op, int fd,
struct epoll_event *event);
op 的选项
名称 | 描述 |
---|---|
EPOLL_CTL_ADD | 把fd指定的文件添加到epfd指定的epoll实例监听集中 |
EPOLL_CTL_DEL | 把fd指定的文件从epfd指定的epoll监听集中删掉 |
EPOLL_CTL_MOD | 使用event改变在已有fd上的监听行为 |
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events参数
名称 | 描述 |
---|---|
EPOLLERR | 文件出错。即使没有设置,这个事件也是被监听的 |
EPOLLET | 在监听文件上开启边沿触发,默认行为是水平触发 |
EPOLLHUP | 文件被挂起,即使没有设置,这个事件也是被监听的 |
EPOLLIN | 文件未阻塞,可读 |
EPOLLONESHOT | 在一次事件产生并处理后,文件不再被监听。(必须指定新事件) |
EPOLLOUT | 文件未阻塞,可写 |
EPOLLPRI | 高优先级的带外数据可读 |
#include <sys/epoll.h>
/* wait for an I/O event on an epoll file descriptor */
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
如果 timeout 为0,即使没有事件发生,调用也立即返回,此时调用返回0。如果 timeout 为 -1,调用将一直等待到有事件发生。
当调用返回,epoll_event 结构体中的 events 字段描述了发生的事件。 data字段包含了所有用户在调用 epoll_ctl()前的设置。
1.2.2 案例
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <cstring>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/epoll.h>
const int BUF_SIZE = 100;
int main(int argc, char** argv)
{
if(argc != 2)
{
printf("Usage %s <port> \n",argv[0]);
return 1;
}
int sock_serv;
// 创建套接字
sock_serv = socket(PF_INET, SOCK_STREAM, 0);
if(sock_serv < 0)
{
perror("socket()");
return 1;
}
// ip,port
struct sockaddr_in serv_add;
memset(&serv_add, 0, sizeof(serv_add));
serv_add.sin_family = AF_INET;
serv_add.sin_addr.s_addr = htonl(INADDR_ANY);
serv_add.sin_port = htons(atoi(argv[1]));
int ret = bind(sock_serv,(struct sockaddr*)&serv_add, sizeof(serv_add));
if(ret < 0)
{
perror("bind()");
return 1;
}
int ret1 = listen(sock_serv, 5);
if(ret1 < 0)
{
perror("listen()");
return 1;
}
int sock_clnt;
// struct timeval mytime;
// fd_set readset;
// FD_ZERO(&readset);
// 把服务器套接字放入监视集合中
// FD_SET(sock_serv,&readset);
// int fd_max = sock_serv;
// fd_set copy_readset;
struct sockaddr_in clnt_addr;
// 创建epoll
int epfd = epoll_create(5);
// 设置事件
struct epoll_event* epevent;//保存返回的事件
struct epoll_event event;//保存一开始的事件
event.events = EPOLLIN;
event.data.fd = sock_serv;
// 给epoll添加描述符和事件
epoll_ctl(epfd, EPOLL_CTL_ADD, sock_serv, &event);
//初始化
epevent =(struct epoll_event*) malloc(sizeof(epevent)*5);
char message[BUF_SIZE];
while(true)
{
// copy_readset = readset;
// mytime.tv_sec = 5;
// mytime.tv_usec = 5000;
// int ans = select(fd_max + 1, ©_readset, NULL, NULL, &mytime);
int epoll_count = epoll_wait(epfd, epevent, 5, -1);
if(epoll_count < 0)
{
perror("epoll_wait()");
break;
}
else if(epoll_count == 0)
{
// printf("time over \n");
continue;
}
else
{
for(int i = 0; i < epoll_count; ++i)
{
// if(FD_ISSET(i, ©_readset))
if(epevent[i].data.fd == sock_serv)
{
// if(i == sock_serv)//sock_serv有请求
// {
socklen_t clnt_size = sizeof(clnt_addr);
sock_clnt = accept(sock_serv, (struct sockaddr*)&sock_clnt,&clnt_size);
if(sock_clnt < 0)
{
continue;
}
// FD_SET(sock_clnt,&readset);
// if(fd_max < sock_clnt)
// {
// fd_max = sock_clnt;
// }
struct epoll_event clnt_event;
clnt_event.data.fd = sock_clnt;
clnt_event.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, sock_clnt, &clnt_event);
printf("Client %d is connecting...\n", sock_clnt);
}
else
{
// int str_len = read(i, message, BUF_SIZE);
int str_len = read(epevent[i].data.fd, message, BUF_SIZE);
if(str_len == 0) //关闭请求
{
// FD_CLR(i, ©_readset);
epoll_ctl(epfd, EPOLL_CTL_DEL, sock_clnt,NULL);
close(i);
printf("close client: %d \n", i);
}
else
{
write(epevent[i].data.fd, message, str_len);
}
}
}
}
}
close(epfd);
close(sock_serv);
return 0;
}
1.2.3边沿触发与条件触发
条件触发方式中,只要输入缓冲有数据就会一直通知该事件。
边缘触发中输入缓冲收到数据时仅注册1次该事件。即使输入缓冲中还留有数据,也不会再注册。边缘触发方式下,以阻塞方式工作的read和write函数有可能引起服务器端的长时间停顿。因此,边缘触发方式中一定要采用非阻塞read和write函数。
将套接字改为非阻塞方式的方法
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
名称 | 含义 |
---|---|
F_GETFL | 获得fd的文件描述符属性 |
F_SETFL | 更改文件描述符属性 |
将文件改为非阻塞模式
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
2.多进程实现并发
看五、多进程服务
3.多线程服务器端
3.1 创建线程
#include <pthread.h>
/*
pthread_t* 传入一个该类型的地址
pthread_attr_t 设置线程的属性
第三个参数:传一个函数,返回类型为void*,参数为void*,
第四个参数:为函数传入参数
*/
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
//Compile and link with -pthread.
3.2 等待线程的消亡
#include <pthread.h>
/*
thread : 线程号
retval : 保存线程函数的返回值。
*/
int pthread_join(pthread_t thread, void **retval);
3. 3 互斥量
#include <pthread.h>
pthread_mutex_t;
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//第二个参数为锁的属性,可以为NULL
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
3.4 信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
//Link with -pthread.
- sem : 创建信号量时传递保存信号量的变量地址值。
- pshared :创建可由多个进程共享的信号量,传递0,创建只允许1个进程内部使用的信号量。
- value :指定新创建的信号量初始值。
int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
使用模板
sem_wait(&sem);
//临界区的开始
.....
//临界区的结束
sem_post(&se)
八、多种I/O函数
1.send(),recv()
#include <sys/types.h>
#include <sys/socket.h>
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);
flags
名称 | 含义 |
---|---|
MSG_OOB | 用于传输带外数据(发送紧急消息) |
MSG_PEEK | 验证输入缓冲中是否存在接收的数据 |
MSG_DONTROUTE | 数据传输过程中不参照路由表在本地网络中寻找目的地 |
MSG_DONTWAIT | 调用I/O函数时不堵塞,用于非堵塞I/O |
MSG_WITALL | 防止函数返回,直到接收全部请求的字节数 |
2.readv(),writev()
也就是说,通过writev函数可以将分散保存在多个缓冲中的数据一并发送,通过readv函数可以由多个缓冲分别接收。因此,适当使用这2个函数可以减少I/0函数的调用次数。
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov,
int iovcnt);
ssize_t writev(int fd, const struct iovec *iov,
int iovcnt);
struct iovec
{
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
九、多播和广播
是基于UDP套接字实现。
1.设置TTL
2.加入多播组
3.广播
十、Linux编程
1.标准IO与系统IO之间的相互转化
#include <stdio.h>
//由fd转化为FILE指针类型
FILE *fdopen(int fd, const char *mode);
//由 FILE指针类型转化为fd
int fileno(FILE *stream)
2.复制文件描述符dup
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
3.实现半关闭
fd转化为FILE类型指针图。
通过复制文件描述符,实现半关闭。
调用shutdown,发送EOF,实现半关闭。