Linux socket编程基础
基础知识
1.主机字节序和网络字节序
主机字节序即内存中存储字节的方式,分为大端序和小端序。何为大端、小端呢?小端:将低字节存储在低地址。大端:将高字节存储在低字节。网络中在处理多字节顺序时一般采用大端序。这也就是说我们在网络传输时需要把做一个主机字节序到网络字节序的转换,实现函数如下:
#include <netinet/in.h>
uint16_t htons(uint16_t <16 位的主机字节序>)
uint32_t htonsl(uint32_t <32 位的主机字节序>) //转换为网络字节序
uint16_t ntohs(uint16_t <16 位的网络字节序>)
uint32_t ntohl(uint32_t <32 位的网络字节序>) //转换为主机字节序
2.缓冲区
每个TCP SOCKET 有一个发送缓冲区和一个接收缓冲区,TCP具有流量控制,所以接收缓冲区的大小就是通知另一端的窗口的大小,对方不会发大于该窗口大小的数据;而 UDP SOCKET 只有一个接收缓冲区无流量控制,当接收的数据报溢出时就会被丢弃。
3.通信域
套接字存在于特定的通信域(即地址族)中,只有隶属于同一地址族的套接字才能建立对话。Linux 支持AF_INET(IPv4 协议)、AF_INET6(IPv6 协议)和 AF_LOCAL(Unix 域协议)。套接口(socket)=网络地址+端口号。要建立一个套接口必须调用socket函数,套接口
有三种类型,即字节流套接口(SOCK_STREAM),数据报套接口(SOCK_DGRAM)和原始套接口(SOCK_RAW)。定义一个连接的一个端点的两元组,即 IP 地址和端口号,称为一个套接口。在网络连接中,两个端点所组成的四元组(即本地IP、本地PORT、远程IP 和远程PORT)称为socket pair,该四元组唯一的标识了一个网络连接。该情况可通过 netstat 验证。
有三种类型,即字节流套接口(SOCK_STREAM),数据报套接口(SOCK_DGRAM)和原始套接口(SOCK_RAW)。定义一个连接的一个端点的两元组,即 IP 地址和端口号,称为一个套接口。在网络连接中,两个端点所组成的四元组(即本地IP、本地PORT、远程IP 和远程PORT)称为socket pair,该四元组唯一的标识了一个网络连接。该情况可通过 netstat 验证。
socket结构
//IPv4 的 Socket 地址结构(定长)
struct in_addr{
In_addr_t s_addr; // 32 位 IP 地址,网络字节序
}
struct sockaddr_in{
Uint8_t sin_len; //IPv4 为固定的 16 字节长度
Sa_family_t sin_family; //地址簇类型,为 AF_INET
In_port_t sin_port; //16 位端口号,网络字节序
struct in_addr sin_addr; // 32 位 IP 地址
char sin_zero[8]; //未用
}
//IPv6 的 socket 地址结构(定长)
struct in6_addr{
uint8_t s6_addr[16]; //128 位 IP 地址,网络字节序
}
struct sockaddr_in6{
uint8_t sin6_len; //IPv6 为固定的 24 字节长度
sa_family_t sin6_family; //地址簇类型,为 AF_INET6
in_port_t sin6_port; //16 位端口号,网络字节序
uint32_t sin6_flowinfo; //32 位流标签
struct in6_addr sin6_addr; //128 位 IP 地址
}
//UNIX 域 socket 地址结构(变长)
struct sockaddr_un //地址簇类型为 AF_LOCAL
//数据链路 socket 地址结构(变长)
struct sockaddr_dl //地址簇类型为 AF_LINK
//通用的 socket 地址结构
struct sockaddr{
uint8_t sa_len;
sa_family_t sa_family;
char sa_data[14];
}
c/s编程
初始化 sock 连接符:
int socket(int domain, int type, int protocol);函数返回 socket 描述符,返回-1 表示出错
domain 参数只能取 AF_INET, protocol 参数一般取 0
应用示例:
TCP 方式:sockfd = socket(AF_INET,SOCK_STREAM,0);
UDP 方式:sockfd =socket(AF_INET, SOCK_DGRAM,0);
绑定端口:
int bind(int sockfd, struct sockaddr *sa, int addrlen);函数返回-1表示出错,最常见的错误是该端口已经被其他程序绑定。
需要注意的一点:在 Linux 系统中,1024 以下的端口只有拥有 root 权限的程序才能绑定。
连接网络(用于 TCP 方式):
int connect(int sockfd, struct sockaddr *servaddr, int addrlen);函数返回-1 表示出错,可能是连接超时或无法访问。返回 0 表示连接成功,可以通过sockfd 传输数据了。
我们说明一下这三个参数,第一个参数是我们调用socket()函数返回的,第二个参数含有目的主机的IP地址和端口号等信息,需要我们自己初始化,第三个参数就是我们初始化的结构体长度。客户在调用connect()之前,不必非得bind(),如果必要的话内核会选择源IP和临时的端口。如果connect()失败,则此socket套接字不能用,必须关闭,不能再调用connect()。所以,当我们调用connect()失败时,必须关闭此套接字,重新调用socket()。
监听端口(用于 TCP 方式):
int listen(int sockfd, int queue_length);需要在此前调用 bind()函数将 sockfd 绑定到一个端口上,否则由系统指定一个随机的端口。
接收队列:一个新的 Client 的连接请求先被放在接收队列中,直到 Server 程序调用 accept 函数接受连接请求。
第二个参数 queue_length,指的就是接收队列的长度也就是在server程序调用 accept 函数之前最大允许的连接请求数,多余的连接请求将被拒绝。
响应连接请求(用于 TCP 方式):
int accept(int sockfd,struct sockaddr *addr,int *addrlen);accept()函数将响应连接请求,建立连接并产生一个新的 socket 描述符来描述该连接,该连接用来与特定的 Client 交换信息。
函数返回新的连接的 socket 描述符,错误返回-1
addr 将在函数调用后被填入请求方的地址信息,如对方的IP、端口等。
addrlen 作为参数表示addr内存区的大小,在函数返回后将被填入返回的addr结构的大小。
accept 默认是阻塞函数,阻塞直到有连接请求。
关闭 socket 连接:
int close(int sockfd);关闭连接将中断对该 socket 的读写操作。
关闭用于 listen()的 socket 描述符将禁止其他 Client 的连接请求。
部分关闭 socket 连接:
int shutdown(int sockfd, int how);Shutdown()函数可以单方面的中断连接,即禁止某个方向的信息传递。
参数 how:
0 - 禁止接收信息
1 - 禁止发送信息
2 - 接收和发送都被禁止,与 close()函数效果相同
接收/发送消息:
TCP 方式:int send(int s, const void *buf, int len, int flags);
int recv(int s, void *buf, int len, int flags);
函数返回实际发送/接收的字节数,返回-1 表示出错,需要关闭此连接。
函数缺省是阻塞函数,直到发送/接收完毕或出错
注意:如果 send 函数返回值与参数 len 不相等,则剩余的未发送信息需要再次发送。
UDP 方式:
int sendto(int s, const void *buf, int len, int flags, const struct sockaddr *to, int tolen);
int recvfrom(int s,void *buf, int len, int flags, struct sockaddr *from, int *fromlen);
与 TCP 方式的区别:
需要指定发送/接收数据的对方(第五个参数 to/from)
函数返回实际发送/接收的字节数,返回-1 表示出错。
函数缺省是阻塞函数,直到发送/接收完毕或出错
注意:如果 send 函数返回值与参数 len 不相等,则剩余的未发送信息需要再次发送。
int sendto(int s, const void *buf, int len, int flags, const struct sockaddr *to, int tolen);
int recvfrom(int s,void *buf, int len, int flags, struct sockaddr *from, int *fromlen);
与 TCP 方式的区别:
需要指定发送/接收数据的对方(第五个参数 to/from)
函数返回实际发送/接收的字节数,返回-1 表示出错。
函数缺省是阻塞函数,直到发送/接收完毕或出错
注意:如果 send 函数返回值与参数 len 不相等,则剩余的未发送信息需要再次发送。
附上以前写的一个函数提供一个解决返回值与len不相等的问题:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <strings.h>
int file_cpy(int fd_src,int fd_des)
{
int sret,dret;
char str[128];
char *p;
bzero(str,128);
while((sret=read(fd_src,str,128))>0){
p=str;
while(sret){
dret=write(fd_des,p,sret);
sret=sret-dret;
p=p+dret;
}
bzero(str,128);
}
return 0;
}
int main(int argc,char *argv[])
{
int fd_src;
int fd_des;
if(argc!=3)
printf("format error\n");
fd_src=open(argv[1],O_RDONLY);
if(fd_src<0)
perror("open fd_src error");
fd_des=open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0666);
if(fd_des<0)
perror("open fd_des error\n");
file_cpy(fd_src,fd_des);
close(fd_des);
close(fd_src);
return 0;
}
上面这四个发送/接收函数均有一个参数flags,用来指明数据发送/接收的标志,常用的标
志主要有:
MSG_PEEK 对数据接收函数有效,表示读出网络数据后不清除已读的数据
MSG_WAITALL 对数据接收函数有效,表示一直执行直到 buf 读满、socket出错或者程序收到信号。
MSG_DONTWAIT 对数据发送函数有效,表示不阻塞等待数据发送完后返回,而是直接返回。(只对非阻塞 socket 有效)
MSG_NOSIGNAL 对发送接收函数有效,表示在对方关闭连接后出错但不发送 SIGPIPE 信号给程序。
MSG_OOB 对发送接收都有效,表示读/写带外数据(out-of-band data)
MSG_WAITALL 对数据接收函数有效,表示一直执行直到 buf 读满、socket出错或者程序收到信号。
MSG_DONTWAIT 对数据发送函数有效,表示不阻塞等待数据发送完后返回,而是直接返回。(只对非阻塞 socket 有效)
MSG_NOSIGNAL 对发送接收函数有效,表示在对方关闭连接后出错但不发送 SIGPIPE 信号给程序。
MSG_OOB 对发送接收都有效,表示读/写带外数据(out-of-band data)
IP地址字符串和网络字节序的二进制IP地址相互转换的函数:
#inlcude <arpa/inet.h>
int inet_aton(const char * <IP 地址字符串>, struct in_addr * <32 位的网络字节序形式的 IP 地址>)
函数返回值:成功返回1,失败返回0。
int inet_aton(const char * <IP 地址字符串>, struct in_addr * <32 位的网络字节序形式的 IP 地址>)
函数返回值:成功返回1,失败返回0。
基本函数介绍完拉,后续我们实现一个socket()文件传输的例子。
server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#define MAXLINE 80
#define SERV_PORT 8000
int file_cpy(int fd_src,int fd_des);
int main(void)
{
struct sockaddr_in servaddr,cliaddr;
socklen_t cliaddr_len;
int listenfd,connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i,n;
int des_fd;
listenfd=socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
servaddr.sin_port=htons(SERV_PORT);
bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
listen(listenfd,20);
printf("Accepting connections ...\n");
while(1){
cliaddr_len=sizeof(cliaddr);
connfd=accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);
read(connfd,buf,MAXLINE);
des_fd=open(buf,O_WRONLY|O_CREAT|O_TRUNC,0666);
if(des_fd<0)
perror("open fd error");
file_cpy(connfd,des_fd);
close(des_fd);
close(connfd);
}
}
int file_cpy(int fd_src,int fd_des)
{
int sret,dret;
char str[128];
char *p;
bzero(str,128);
while((sret=read(fd_src,str,128))>0){
p=str;
while(sret){
dret=write(fd_des,p,sret);
sret=sret-dret;
p=p+dret;
}
bzero(str,128);
}
return 0;
}
client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#define MAXLINE 80
#define SERV_PORT 8000
int file_cpy(int fd_src,int fd_des);
int main(int argc,char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd,n;
char *str;
int fd;
if(argc !=2){
fputs("usage: ./client file_name\n",stderr);
exit(1);
}
str = argv[1];
sockfd=socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
inet_pton(AF_INET,"127.0.0.1",&servaddr.sin_addr);
servaddr.sin_port=htons(SERV_PORT);
connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
write(sockfd,str,strlen(str)+1);
fd=open(str,O_RDONLY);
if(fd<0)
perror("open fd error");
file_cpy(fd,sockfd);
close(fd);
close(sockfd);
return 0;
}
int file_cpy(int fd_src,int fd_des)
{
int sret,dret;
char str[128];
char *p;
bzero(str,128);
while((sret=read(fd_src,str,128))>0){
p=str;
while(sret){
dret=write(fd_des,p,sret);
sret=sret-dret;
p=p+dret;
}
bzero(str,128);
}
return 0;
}
使用fork()并发处理多个client请求,服务器端代码如下,客户端代码不变
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#define MAXLINE 80
#define SERV_PORT 8000
int file_cpy(int fd_src,int fd_des);
int main(void)
{
struct sockaddr_in servaddr,cliaddr;
socklen_t cliaddr_len;
int listenfd,connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int n;
int des_fd;
pid_t pid;
listenfd=socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
servaddr.sin_port=htons(SERV_PORT);
bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
listen(listenfd,20);
printf("Accepting connections ...\n");
while(1){
cliaddr_len=sizeof(cliaddr);
connfd=accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);
pid=fork();
if(pid<0){
perror("fork");
exit(1);
}
if(pid==0){
sleep(10);
close(listenfd);
while(1){
n=read(connfd,buf,MAXLINE);
if(n==0){
printf("the other side closed\n");
break;
}
printf("received from %s at PORT %d\n",inet_ntop(AF_INET,&cliaddr.sin_addr,str,sizeof(str)),ntohs(cliaddr.sin_port));
des_fd=open(buf,O_WRONLY|O_CREAT|O_TRUNC,0666);
if(des_fd<0)
perror("open fd error");
file_cpy(connfd,des_fd);
close(des_fd);
}
close(connfd);
exit(0);
}
else
close(connfd);
}
}
int file_cpy(int fd_src,int fd_des)
{
int sret,dret;
char str[128];
char *p;
bzero(str,128);
while((sret=read(fd_src,str,128))>0){
p=str;
while(sret){
dret=write(fd_des,p,sret);
sret=sret-dret;
p=p+dret;
}
bzero(str,128);
}
return 0;
}