网络编程套接字(3)——socket读写数据接口API
对文件的读写操作read和write同样适用于socket。但是socket编程接口提供了专门的几个socket读写数据的接口。
UDP数据读写函数
recvfrom(从另一个IP接收数据)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
- sockfd:之前创建的socket文件描述符,从这个文件描述符中读取数据
- buf:用来存放接收数据的缓冲区
- len:该缓冲区的大小,这里注意有坑,需要用sizeof(buf)-1,因为这里还需要一个空间的大小的来存放'\0'。
- flags:一般设置为0,具体用法下面会讲到
- src_addr:因为UDP没有通信连接的概念,因此每次接收数据都需要获取到发送端的socket地址(下面的sendto函数也是类似),src_addr就是发送端的源地址,对于服务器来说,这也就是客户端来发送的地址。从sockfd中读取到发送端的socket地址放到这个src_addr结构体中
- addrlen:源地址结构体的大小,注意这里的类型是socklen_t*,需要&sizeof(src_addr),这也是一个输入输出型参数(关于输入输出型的参数意思,和上面的accept的第三个参数类似)
- 返回值:失败返回-1,成功返回实际接收数据的大小。
sendto(发送数据给另一个IP)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
- sockfd:之前创建的socket文件描述符
- buf:要发送的数据在buf当中
- len:这里和recvfrom不同的是,此处不需要sizeof(buf)-1,因为这里的buf数组可能不是满的,它的中间可能有'\0',sizeof(buf)得到的是整个缓冲区的大小,strlen(buf)得到的大小是到第一个'\0'的大小,所以此处需要用的是strlen(buf)
- flags:一般设置为0,具体用法下面会讲到
- dest_addr:指要把数据发送给哪个目的IP,如果是客户端给服务器发消息,那么dest_addr就是服务器的socket地址
- addrlen:目的IP的大小,注意这里又有和recvfrom不同的地方,前面的addrlen的类型的是指针,这里不是指针,所以直接用sizeof(dest_addr)即可,因为这不是一个输入输出型参数
- 返回值:失败返回-1,成功返回传送的数据的大小
注意:recvfrom函数和sendto函数还可以用于面向连接的socket读写,只需要把最后两个参数都设置为NULL就可以了,因为我们已经知道对端的socket地址了,这就有点像下面要将的recv和send了。
下面贴上UDP协议的客户端和服务器的代码
server.c
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
3.
#include <arpa/inet.h>
#include <stdlib.h>
//一个服务器程序的典型逻辑
//1.服务器的初始化和启动(指定IP地址和端口号,加载需要的数据文件)
//2.进入事件循环(死循环),无限等待客户需求
// a).读取客户端发送的数据
// b).根据客户端发送的数据进行计算(对于不同用途的服务器,计算的逻辑不同,其中的过程可能很复杂,涉及到几十台服务器之间的相互配合)
// c).根据计算出来的不同结果拼接相应的字符串
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
// ./server [ip][port]
// ./server 192.168.224.136 9090
int main(int argc,char *argv[]){
//如果给的参数不是3个,报错返回
if(argc != 3){
printf("Usage ./server [ip][port]\n");
return 1;
}
int fd = socket(AF_INET,SOCK_DGRAM,0);//创建socket文件描述符
if(fd < 0){
perror("socket");
return 1;
}
//创建一个IPv4的网络编程结构体
sockaddr_in addr;
addr.sin_family = AF_INET;//sin_family指定为AF_INET,表示用的IPv4结构
addr.sin_addr.s_addr = inet_addr(argv[1]);
//把下标为1的参数的字符串转化为in_addr的结构,in_addr用来表示一个IPv4的IP地址,其实就是一个32位整数(把点分十进制转化为32位整数)
//在实际传参的时候,这个参数通常设为0,表示本地的任意IP地址
//因为服务器这个机器上可能有多个网卡,每个网卡也可能绑定多个IP地址
//这样设置可以在所有的IP地址上都能收到请求,直到与某个客户端建立了连接以后才确定接下来到底用哪个IP地址
addr.sin_port = htons(atoi(argv[2]));
//把下标为2的参数先用aoti从字符串转化为数字,然后用htons将16位短整数数字从主机字节序转化为网络字节序
int ret = bind(fd,(sockaddr*)&addr,sizeof(addr));//将IP地址和端口号与文件描述符关联起来
if(ret < 0){
perror("bind");
return 1;
}
//bind之后就可以直接通信了
//对于服务器来说,程序要一直循环下去,需要一个while(1)死循环
while(1){
sockaddr_in client_addr;//创建一个client IPv4结构体
socklen_t len=sizeof(client_addr);//用len记下client的长度,之后有用
printf(">: ");
fflush(stdout);
char buf[1024]={0};
ssize_t s = recvfrom(fd,buf,sizeof(buf)-1,0,(sockaddr*)&client_addr,&len);
//从client服务器中接收数据到buf中
//因为这里需要知道发送端的socket地址,所以需要创建一个client_addr,这里创建的是IPv4
//从fd中读到的发送端的socker地址就放到client_addr这个结构体中
//&len是一个输入输出型参数,输入的是client_addr结构体的大小,返回的是实际接收到的socket地址类型的结构体大小
if(s < 0){
perror("recvfrom");
continue;//这里不能退出,需要continue,不能让这个死循环停下
}
buf[s] = '\0';//s的大小表示接受了多少数据
printf("[%s:%d]:%s\n",inet_ntoa(client.sin_addr),
ntohs(client.sin_port),buf);
//第一个参数是将客户端的IP地址,通过inet_ntoa函数转化为字符串输出来,
//第二个参数代表的是通过ntohs函数,将客户端传过来的端口号把其网络字节序转化为主机字节序
//第三个参数代表的是直接从客户端接收到buf数组当中的内容
//接下来就是服务器接收到客户端请求后的相应,对于不同的服务器来说,要根据不同的请求(request),做出不同的相应(response)
//此处由于我们只实现最简单的echo服务器,就不涉及复杂的计算了,直接相应客户端传给服务器的数据
sendto(fd,buf,strlen(buf),0,(sockaddr*)&client_addr,sizeof(client_addr));
//将客户端传过来存在buf中的数据原模原样发送给客户端
//用strlen(buf)表示缓冲区实际存放的数据的大小
//那么此时发送端的socket地址就是前面的client_addr
//这里的sizeof(client_addr)就不是一个输入输出型参数了,因此它不是一个地址,是值传递
}
return 0;
}
client.c
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
int main(int argc,char *argv[])
{
if(argc != 3){
printf("Usage ./client [ip][port]\n");
return 1;
}
int fd = socket(AF_INET,SOCK_DGRAM,0);//创建一个socket网络通讯端口
if(fd < 0){
perror("socket");
return 1;
}
//下面的几步都和服务器当中的意思相同
//只不过要注意的是,我们这里的这个server_addr实际上服务器
//在传参的时候,argv[1]传的应该是一个127.0.0.1这样的本地地址
//argv[2]传的是服务器绑定的端口号
sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
server_addr.sin_port = htons(atoi(argv[2]));
while(1){
char buf[1024]={0};
printf("Please Enter# ");
fflush(stdout);
socklen_t len=sizeof(server_addr);
ssize_t read_size = read(0,buf,sizeof(buf)-1);
//从标准输入中读取数据到buf中
if(read_size < 0){
perror("read");
return 1;
}
buf[read_size]='\0';
ssize_t write_size = sendto(fd,buf,strlen(buf),0,(sockaddr*)&server_addr,sizeof(server_addr));
//从buf中读取数据发给server_addr
if(write_size < 0){
perror("sendto");
return 1;
}
char buf_recv[1024]={0};
read_size = recvfrom(fd,buf_recv,sizeof(buf_recv)-1,0,(sockaddr*)&server_addr,&len);
//接收从server_client传回来的数据
if(read_size < 0 ){
perror("recvfrom");
return 1;
}
buf_recv[read_size]='\0';
printf("server echo# %s\n",buf);
//输出buf中存放的服务器发来的数据
}
return 0;
}
执行结果
多个客户端同时请求也是可以的
client进程的端口号49052,40911,34589是系统自动分配的
关于两个函数的flags具体取值,有下面这些取法
选项名 |
含义 |
sendto(send) |
recvfrom(recv) |
MSG_CONFIRM | 指示数据链路层协议持续监听对方的回应,知道得到对方的答复。它仅能用于SOCK_DGRAM和SOCK_RAW类型的socket |
Y |
N |
MSG_DONTROUTE |
不查看路由表,直接将数据发送给本地局域网络内的主机。这表示发送发明确知道目标主机就在本地网络上 |
Y |
N |
MSG_DONTWAIT |
对socket的此次操作将是非阻塞的 |
Y |
Y |
MSG_MORE |
告诉内核应用程序还有更过数据要发送,内核将超时等待新数据写入TCP发送缓冲区后一并发送。这样可防止TCP发送过多小的报文段,从而提高传输效率 |
Y |
N |
MSG_WAITALL |
读操作仅在读取到指定数量的字节后才返回(就是s读到第三次参数指定的len大小后才返回) | N |
Y |
MSG_PEEK |
窥探读缓冲中的数据,此次读操作不会导致这些数据被清除(只是看,不读出来) |
N |
Y |
MSG_OOB |
发送或接收紧急数据 |
Y |
Y |
MSG_NOSIGNAL |
往读端关闭的管道或者socket连接中写数据时不引发SIGPIPE信号 |
Y |
N |
TCP数据读写函数
recv(从socket中读取数据)
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- sockfd是之前创建的文件描述符,并且是已经命名过(bind)和设置监听了的(listen)
- buf:用来存放接收数据的缓冲区
- len:该缓冲区的大小,注意和recvfrom中的一样也要留一个空间给'\0'
- flags:同recvfrom
send(往socket中写入数据)
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- sockfd是之前创建的文件描述符,并且是已经命名过(bind)和设置监听了的(listen)
- buf:发送的是buf缓冲区中的数据
- len:缓冲区实际的有效大小(字符串就用strlen)
- flags:同sendto
下面贴上TCP协议的客户端和服务器的代码(这里用的是read和write,这只是个人习惯,都可以的)
server.c
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
int main(int argc,char* argv[]){
if(argc != 3){
printf("Usage ./server [ip][port]\n");
return 1;
}
int fd=socket(AF_INET,SOCK_STREAM,0);//创建一个socket文件描述符,SOCK_STREAMBIAOSHI面向字节流
if(fd < 0){
perror("socket");
return 1;
}
sockaddr_in server_addr;
server_addr.sin_family=AF_INET;
server_addr.sin_addr .s_addr=inet_addr(argv[1]);
server_addr.sin_port=htons(atoi(argv[2]));
int ret = bind(fd,(sockaddr*)&server_addr,sizeof(server_addr));
if(ret < 0){
perror("bind");
close(fd);
return 1;
}
ret = listen(fd,5);//开启监听
if(ret < 0){
perror("listen");
close(fd);
return 1;
}
printf("bind and listen success,wait accept....\n");
while(1){
sockaddr_in client_addr;//创建一个客户端IP
socklen_t len = sizeof(client_addr);
int client_sock = accept(fd,(sockaddr*)&client_addr,&len);//接收客户端的连接
if(client_sock < 0){
perror("accept");
close(fd);
continue;
}
printf("connected,ip is %s,port is %d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
//连接成功之后执行下列执行流进行响应
while(1){
printf("> ");
fflush(stdout);
char buf[1024]={0};
ssize_t read_size = recvfrom(client_sock,buf,sizeof(buf)-1,0,(sockaddr*)&client_addr,&len);
if(read_size < 0){
perror("read");
close(fd);
return 1;
}
buf[read_size]='\0';
printf("client :# %s",buf);
sendto(client_sock,buf,strlen(buf),0,(sockaddr*)&client_addr,len);
printf("Please wait.....\n");
}
}
close(fd);
return 0;
}
client.c
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
int main(int argc,char *argv[]){
if(argc != 3){
printf("Usage ./client [ip][port]\n");
return 1;
}
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd < 0){
perror("socket");
return 1;
}
sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
server_addr.sin_port = htons(atoi(argv[2]));
int ret = connect(fd,(sockaddr*)&server_addr,sizeof(server_addr));
if(ret < 0){
perror("connect");
close(fd);
return 1;
}
printf("connect success......\n");
while(1){
printf("[client]:");
fflush(stdout);
char buf[1024]={0};
ssize_t read_size = read(0,buf,sizeof(buf)-1);
if(read_size < 0){
perror("read");
close(fd);
return 1;
}
buf[read_size]='\0';
ssize_t write_size =sendto(fd,buf,strlen(buf),0,(sockaddr*)&server_addr,sizeof(server_addr));
if(write_size < 0){
perror("sendto");
close(fd);
return 1;
}
char buf_recv[1024]={0};
socklen_t len = sizeof(server_addr);
read_size = recvfrom(fd,buf_recv,sizeof(buf_recv)-1,0,(sockaddr*)&server_addr,&len);
if(read_size < 0){
perror("recvfrom");
close(fd);
return 1;
}
buf_recv[read_size]='\0';
printf("[server]: %s\n",buf_recv);
}
close(fd);
return 0;
}
运行结果如下
注意:再启动一个客户端,尝试连接服务器,发现第二个客户端,不能正确的和服务器进行通信。分析原因,是因为我们accept了一个请求之后,就在一直while循环尝试read,没有继续调用到accept,导致不能接受新的请求所以这是不科学的,需要采用下面的多进程和多线程版本的TCP网络通信。
TCP网络程序(多进程版,client.c与上面的一样)
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
void ProcessRequest(int client_fd,sockaddr_in* client_addr){
while(1){
char buf[1024]={0};
ssize_t read_size = read(client_fd,buf,sizeof(buf)-1);
if(read_size < 0){
perror("read");
continue;
}
else if(read_size == 0){
//表示客户端输入完毕
printf("[client] [%s] [%d] say bye!\n",
inet_ntoa(client_addr->sin_addr),ntohs(client_addr->sin_port));
close(client_fd);
break;
}
else{
buf[read_size] = '\0';
printf("[client] [%s] [%d] say %s\n",
inet_ntoa(client_addr->sin_addr),ntohs(client_addr->sin_port),buf);
write(client_fd,buf,strlen(buf));
}
}
}
void CreateWorker(int client_fd,sockaddr_in* client_addr){
pid_t pid = fork();
if(pid < 0){
perror("fork");
return ;
}
else if(pid == 0){
//子进程
if(fork() == 0){
//孙子进程
ProcessRequest(client_fd,client_addr);
}
exit(0);
}
else{
//父进程
close(client_fd);
waitpid(pid,NULL,0);
}
}
int main(int argc,char* argv[]){
if(argc != 3){
printf("Usage ./server [ip][port]\n");
return 1;
}
int fd=socket(AF_INET,SOCK_STREAM,0);//创建一个socket文件描述符,SOCK_STREAMBIAOSHI面向字节流
if(fd < 0){
perror("socket");
return 1;
}
sockaddr_in server_addr;
server_addr.sin_family=AF_INET;
server_addr.sin_addr .s_addr=inet_addr(argv[1]);
server_addr.sin_port=htons(atoi(argv[2]));
int ret = bind(fd,(sockaddr*)&server_addr,sizeof(server_addr));
if(ret < 0){
perror("bind");
close(fd);
return 1;
}
ret = listen(fd,5);//开启监听
if(ret < 0){
perror("listen");
close(fd);
return 1;
}
printf("bind and listen success,wait accept....\n");
while(1){
sockaddr_in client_addr;//创建一个客户端IP
socklen_t len = sizeof(client_addr);
int client_fd = accept(fd,(sockaddr*)&client_addr,&len);//接收客户端的链接
if(client_fd < 0){
perror("accept");
close(fd);
continue;
}
CreateWorker(client_fd,&client_addr);
}
close(fd);
return 0;
}
这里接受请求时,先由父进程创建了一个子进程,再由子进程创建了一个孙子进程,由孙子进程完成与客户端的交互。子进程一旦创建完孙子进程就退出,此时父进程wait到了子进程,执行完剩下的代码并退出。而此时,孙子进程因为子进程已经退出了,所以成为了僵尸进程,由系统1号进程进行回收孙子进程
TCP网络通信(多线程版)
#include <stdio.h>
#include <sys/socket.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
void ProcessRequest(int client_fd,sockaddr_in* client_addr){
while(1){
char buf[1024]={0};
ssize_t read_size = read(client_fd,buf,sizeof(buf)-1);
if(read_size < 0){
perror("read");
continue;
}
else if(read_size == 0){
//表示客户端输入完毕
printf("[client] [%s] [%d] say bye!\n",inet_ntoa(client_addr->sin_addr),ntohs(client_addr->sin_port));
close(client_fd);
break;
}
else{
buf[read_size] = '\0';
printf("[client] [%s] [%d] say %s\n",inet_ntoa(client_addr->sin_addr),ntohs(client_addr->sin_port),buf);
write(client_fd,buf,strlen(buf));
}
}
}
typedef struct Arg{
sockaddr_in client_addr;
int client_fd;
}Arg;
void* CreateWorker(void *ptr){
Arg* arg = (Arg*)ptr;
ProcessRequest(arg->client_fd,&arg->client_addr);
free(arg);
return NULL;
}
int main(int argc,char* argv[]){
if(argc != 3){
printf("Usage ./server [ip][port]\n");
return 1;
}
int fd=socket(AF_INET,SOCK_STREAM,0);//创建一个socket文件描述符,SOCK_STREAMBIAOSHI面向字节流
if(fd < 0){
perror("socket");
return 1;
}
sockaddr_in server_addr;
server_addr.sin_family=AF_INET;
server_addr.sin_addr .s_addr=inet_addr(argv[1]);
server_addr.sin_port=htons(atoi(argv[2]));
int ret = bind(fd,(sockaddr*)&server_addr,sizeof(server_addr));
if(ret < 0){
perror("bind");
close(fd);
return 1;
}
ret = listen(fd,5);//开启监听
if(ret < 0){
perror("listen");
close(fd);
return 1;
}
printf("bind and listen success,wait accept....\n");
while(1){
sockaddr_in client_addr;//创建一个客户端IP
socklen_t len = sizeof(client_addr);
int client_fd = accept(fd,(sockaddr*)&client_addr,&len);//接收客户端的链接
if(client_fd < 0){
perror("accept");
close(fd);
continue;
}
pthread_t tid;
Arg *arg=(Arg*)malloc(sizeof(Arg));
arg->client_fd = client_fd;
arg->client_addr = client_addr;
pthread_create(&tid,NULL,CreateWorker,(void*)arg);
pthread_detach(tid);
}
close(fd);
return 0;
}
通用数据读写函数
socket编程接口还提供了一对通用的数据读写接口。它们不仅能用于TCP,也能用于UDP
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
- sockfd参数指定被操作的目标socket
- msg参数是msghdr结构体指针,该结构体定义如下
struct msghdr {
void* msg_name; /* socket地址 */
socklen_t msg_namelen; /* socket地址长度 */
struct iovec* msg_iov; /* 分散的内存块 */
size_t msg_iovlen; /* 分散的内存块数量 */
void* msg_control; /* 指向辅助数据的起始位置 */
size_t msg_controllen; /* 辅助数据的大小 */
int msg_flags; /* 复制函数中的flags参数,并在调用过程中更新 */
};
struct iovec {
void *iov_base; /* 内存起始地址 */
size_t iov_len; /* 这块内存的长度 */
};
- msg_name成员指向一个socket地址结构变量。它指定通信对方的socket地址。对于面向连接的TCP协议,该成员没有意义,必须被置为NULL
- msg_namelen成员指定了msg_name成员所指的socket地址的长度
- msg_iov成员指向一个iovec结构体,该结构体封装了一块内存的起始位置和长度
- msg_iovlen指定这样的结构体有几个。对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度由msg_iov指向的数组指定,这称为分散读;对于sengmsg而言,msg_ioven块分散内存中的数据将被一并发送,这称为集中写。
- msg_control和msg_controllen用于辅助数据的传送
- msg_flags成员无需设定,它会复制传入的flags参数。recvmsg还会在调用结束前,将某些更新后的标志设置到msg_flags中。