Linux中的网络编程通过Socket(套接字)接口实现,Socket是一种文件描述符。
类型:
套接字socket有三种类型:
1)流式套接字(SOCK_STREAM),流式的套接字可以提供可靠的、面向连接的通讯流。它使用了TCP协议。TCP保证了数据传输的正确性和顺序性。
2)数据报套接字(SOCK_DGRAM),数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错,它使用数据报协议UDP。
3)原始套接字,原始套接字允许对低层协议如IP或ICMP直接访问,主要用于新的网络协议的测试等。
地址结构:
struct sockaddr
{
u_short sa_family;
char sa_data[14];
}
sa_family:地址族,采用“AF_xxx”的形式,如:AF_INET。
sa_data:14字节的特定协议地址。
struct sockaddr_in
{
short int sin_family; /* Internet地址族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP地址 */
unsigned char sin_zero[8]; /* 填0 */
}
struct in_addr
{
unsigned long s_addr;
}
s_addr: 32位的地址。
地址转换:
IP地址通常由数字加点(192.168.0.1)的形式表示,而在struct in_addr中使用的是IP地址是由32位的整数表示的,为了转换我们可以使用下面两个函数:
int inet_aton(const char *cp,struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
函数里面 a 代表 ascii n 代表network.第一个函数表示将a.b.c.d形式的IP转换为32位的IP,存储在 inp指针里面。第二个是将32位IP转换为a.b.c.d的格式。
字节序转换:
不同类型的 CPU 对变量的字节存储顺序可能不同:有的系统是高位在前,低位在后,而有的系统是低位在前,高位在后,而网络传输的数据顺序是一定要统一的。所以当内部字节存储顺序和网络字节顺序不同时,就一定要进行转换。
网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。
htons:
把unsigned short类型从主机序转换到网络序
htonl:
把unsigned long类型从主机序转换到网络序
ntohs:
把unsigned short类型从网络序转换到主机序
ntohl:
把unsigned long类型从网络序转换到主机序
IP与主机名:
在网络上标识一台机器可以用IP,也可以使用主机名。
struct hostent *gethostbyname(const char *hostname)
struct hostent
{
char *h_name; /* 主机的正式名称 */
char *h_aliases; /* 主机的别名 */
int h_addrtype; /* 主机的地址类型 AF_INET*/
int h_length; /* 主机的地址长度 */
char **h_addr_list; /* 主机的IP地址列表 */
}
#define h_addr h_addr_list[0] /* 主机的第一个IP地址*/
函数:
进行Socket编程的常用函数有:
• socket
创建一个socket。
• bind
用于绑定IP地址和端口号到socket。
• connect
该函数用于绑定之后的client端,与服务器建立连接。
• listen
设置能处理的最大连接要求,Listen()并未开始接收连线,只是设置socket为listen模式。
• accept
用来接受socket连接。
• send
发送数据
• recv
接收数据
基于TCP-服务器:
1. 创建一个socket,用函数socket()
2. 绑定IP地址、端口等信息到socket上,用函数bind()
3. 设置允许的最大连接数,用函数listen()
4. 接收客户端上来的连接,用函数accept()
5. 收发数据,用函数send()和recv(),或者read()和write()
6. 关闭网络连接
基于TCP-客户端:
1. 创建一个socket,用函数socket()
2. 设置要连接的对方的IP地址和端口等属性
3. 连接服务器,用函数connect()
4. 收发数据,用函数send()和recv(),或者read()和write()
5. 关闭网络连接
tcp_client.c
#include <stdlib.h>tcp_server.c
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define portnumber 3333
int main(int argc, char *argv[])
{
int sockfd;
char buffer[1024];
struct sockaddr_in server_addr;
struct hostent *host;
if(argc!=2)
{
fprintf(stderr,"Usage:%s hostname \a\n",argv[0]);
exit(1);
}
/* 使用hostname查询host 名字 */
if((host=gethostbyname(argv[1]))==NULL)
{
fprintf(stderr,"Gethostname error\n");
exit(1);
}
/* 客户程序开始建立 sockfd描述符 */
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) // AF_INET:Internet;SOCK_STREAM:TCP
{
fprintf(stderr,"Socket Error:%s\a\n",strerror(errno));
exit(1);
}
/* 客户程序填充服务端的资料 */
bzero(&server_addr,sizeof(server_addr)); // 初始化,置0
server_addr.sin_family=AF_INET; // IPV4
server_addr.sin_port=htons(portnumber); // (将本机器上的short数据转化为网络上的short数据)端口号
server_addr.sin_addr=*((struct in_addr *)host->h_addr); // IP地址
/* 客户程序发起连接请求 */
if(connect(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
{
fprintf(stderr,"Connect Error:%s\a\n",strerror(errno));
exit(1);
}
/* 连接成功了 */
printf("Please input char:\n");
/* 发送数据 */
fgets(buffer,1024,stdin);
write(sockfd,buffer,strlen(buffer));
/* 结束通讯 */
close(sockfd);
exit(0);
}
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define portnumber 3333
int main(int argc, char *argv[])
{
int sockfd,new_fd;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
int sin_size;
int nbytes;
char buffer[1024];
/* 服务器端开始建立sockfd描述符 */
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) // AF_INET:IPV4;SOCK_STREAM:TCP
{
fprintf(stderr,"Socket error:%s\n\a",strerror(errno));
exit(1);
}
/* 服务器端填充 sockaddr结构 */
bzero(&server_addr,sizeof(struct sockaddr_in)); // 初始化,置0
server_addr.sin_family=AF_INET; // Internet
server_addr.sin_addr.s_addr=htonl(INADDR_ANY); // (将本机器上的long数据转化为网络上的long数据)服务器程序能运行在任何ip的主机上 //INADDR_ANY 表示主机可以是任意IP地址,即服务器程序可以绑定到所有的IP上
//server_addr.sin_addr.s_addr=inet_addr("192.168.1.1"); //用于绑定到一个固定IP,inet_addr用于把数字加格式的ip转化为整形ip
server_addr.sin_port=htons(portnumber); // (将本机器上的short数据转化为网络上的short数据)端口号
/* 捆绑sockfd描述符到IP地址 */
if(bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
{
fprintf(stderr,"Bind error:%s\n\a",strerror(errno));
exit(1);
}
/* 设置允许连接的最大客户端数 */
if(listen(sockfd,5)==-1)
{
fprintf(stderr,"Listen error:%s\n\a",strerror(errno));
exit(1);
}
while(1)
{
/* 服务器阻塞,直到客户程序建立连接 */
sin_size=sizeof(struct sockaddr_in);
if((new_fd=accept(sockfd,(struct sockaddr *)(&client_addr),&sin_size))==-1)
{
fprintf(stderr,"Accept error:%s\n\a",strerror(errno));
exit(1);
}
fprintf(stderr,"Server get connection from %s\n",inet_ntoa(client_addr.sin_addr)); // 将网络地址转换成.字符串
if((nbytes=read(new_fd,buffer,1024))==-1)
{
fprintf(stderr,"Read Error:%s\n",strerror(errno));
exit(1);
}
buffer[nbytes]='\0';
printf("Server received %s\n",buffer);
/* 这个通讯已经结束 */
close(new_fd);
/* 循环下一个 */
}
/* 结束通讯 */
close(sockfd);
exit(0);
}
基于UDP-服务器
1. 创建一个socket,用函数socket()
2. 绑定IP地址、端口等信息到socket上,用函数bind()
3. 循环接收数据,用函数recvfrom()
4. 关闭网络连接基于
UDP-客户端
1. 创建一个socket,用函数socket()
2. 绑定IP地址、端口等信息到socket上,
用函数bind()
3. 设置对方的IP地址和端口等属性
4. 发送数据,用函数sendto()
5. 关闭网络连接
udp_client.c
#include <stdlib.h>udp_server.c
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <arpa/inet.h>
#define SERVER_PORT 8888
#define MAX_BUF_SIZE 1024
void udpc_requ(int sockfd,const struct sockaddr_in *addr,int len)
{
char buffer[MAX_BUF_SIZE];
int n;
while(1)
{ /* 从键盘读入,写到服务端 */
printf("Please input char:\n");
fgets(buffer,MAX_BUF_SIZE,stdin);
sendto(sockfd,buffer,strlen(buffer),0,(struct sockaddr *)addr,len);
bzero(buffer,MAX_BUF_SIZE);
}
}
int main(int argc,char **argv)
{
int sockfd;
struct sockaddr_in addr;
if(argc!=2)
{
fprintf(stderr,"Usage:%s server_ip\n",argv[0]);
exit(1);
}
/* 建立 sockfd描述符 */
sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0)
{
fprintf(stderr,"Socket Error:%s\n",strerror(errno));
exit(1);
}
/* 填充服务端的资料 */
bzero(&addr,sizeof(struct sockaddr_in));
addr.sin_family=AF_INET;
addr.sin_port=htons(SERVER_PORT);
if(inet_aton(argv[1],&addr.sin_addr)<0) /*inet_aton函数用于把字符串型的IP地址转化成网络2进制数字*/
{
fprintf(stderr,"Ip error:%s\n",strerror(errno));
exit(1);
}
udpc_requ(sockfd,&addr,sizeof(struct sockaddr_in)); // 进行读写操作
close(sockfd);
}
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <arpa/inet.h>
#define SERVER_PORT 8888
#define MAX_MSG_SIZE 1024
void udps_respon(int sockfd)
{
struct sockaddr_in addr;
int addrlen,n;
char msg[MAX_MSG_SIZE];
while(1)
{/* 从网络上读,并写到网络上 */
bzero(msg,sizeof(msg)); // 初始化,清零
addrlen = sizeof(struct sockaddr);
n=recvfrom(sockfd,msg,MAX_MSG_SIZE,0,(struct sockaddr*)&addr,&addrlen); // 从客户端接收消息
msg[n]=0;//将收到的字符串尾端添加上字符串结束标志
/* 显示服务端已经收到了信息 */
fprintf(stdout,"Server have received %s",msg); // 显示消息
}
}
int main(void)
{
int sockfd;
struct sockaddr_in addr;
/* 服务器端开始建立socket描述符 */
sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0)
{
fprintf(stderr,"Socket Error:%s\n",strerror(errno));
exit(1);
}
/* 服务器端填充 sockaddr结构 */
bzero(&addr,sizeof(struct sockaddr_in));
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=htonl(INADDR_ANY);
addr.sin_port=htons(SERVER_PORT);
/* 捆绑sockfd描述符 */
if(bind(sockfd,(struct sockaddr *)&addr,sizeof(struct sockaddr_in))<0)
{
fprintf(stderr,"Bind Error:%s\n",strerror(errno));
exit(1);
}
udps_respon(sockfd); // 进行读写操作
close(sockfd);
}
服务器模型:
在网络程序里面,一般来说都是许多客户对应一个服务器,为了处理客户的请求, 对服务端的程序就提出了特殊的要求。目前最常用的服务器模型有:
•循环服务器:服务器在同一个时刻只可以响应一个客户端的请求
•并发服务器:服务器在同一个时刻可以响应多个客户端的请求
UDP循环服务器
UDP循环服务器的实现方法:UDP服务器每次从套接字上读取一个客户端的请求->处理->然后将结果返回给客户机。
socket(...);
bind(...);
while(1)
{
recvfrom(...);
process(...);
sendto(...);
}
因为UDP是非面向连接的,没有一个客户端可以老是占住服务端, 服务器对于每一个客户机的请求总是能够满足。
TCP循环服务器
TCP循环服务器一次只能处理一个客户端的请求。只有在这个客户的所有请求都满足后, 服务器才可以继续后面的请求。这样如果有一个客户端占住服务器不放时,其它的客户机都不能工作了,因此,TCP服务器一般很少用循环服务器模型的。
TCP并发服务器
并发服务器的思想是每一个客户机的请求并不由服务器直接处
理,而是由服务器创建一个 子进程来处理。算法如下:
socket(...);
bind(...);
listen(...);
while(1) {
accept(...);
if(fork(..)==0) {
process(...);
close(...);
exit(...);
}
close(...);
}
TCP并发服务器可以解决TCP循环服务器客户机独占服务器的情况。但同时也带来了问题:为了响应客户的请求,服务器要创建子进程来处理,而创建子进程是一种非常消耗资源的操作。
多路复用I/O:
阻塞函数在完成其指定的任务以前不允许程序继续向下执行。例如:当服务器运行到accept语句时,而没有客户请求连接,服务器就会停止在accept语句上等待连接请求的到来。这种情况称为阻塞(blocking),而非阻塞操作则可以立即完成。例如,如果你希望服务器仅仅检查是否有客户在等待连接,有就接受连接,否则就继续做其他事情,则可以通过使用select系统调用来实现。除此之外,select还可以同时监视多个套接字 。
int select(int maxfd, fd_set *readfds, fd_set *writefds, fe_set
*exceptfds, const struct timeval *timeout)
Maxfd: 文件描述符的范围,比待检的最大文件描述符大1
Readfds:被读监控的文件描述符集
Writefds:被写监控的文件描述符集
Exceptfds:被异常监控的文件描述符集
Timeout:定时器
Timeout取不同的值,该调用有不同的表现:
Timeout值为0,不管是否有文件满足要求,都立刻返回,无文件满足要求返回0,有文件满足要求返回一个正值。
Timeout为NULL,select将阻塞进程,直到某个文件满足要求
Timeout值为正整数,就是等待的最长时间,即select在timeout时间内阻塞进程。
Select调用返回时,返回值有如下情况:
1. 正常情况下返回满足要求的文件描述符个数;
2. 经过了timeout等待后仍无文件满足要求,返回值为0;
3. 如果select被某个信号中断,它将返回-1并设置errno为EINTR。
4. 如果出错,返回-1并设置相应的errno。
系统提供了4个宏对描述符集进行操作:
#include <sys/select.h>
void FD_SET(int fd, fd_set *fdset)
void FD_CLR(int fd, fd_set *fdset)
void FD_ZERO(fd_set *fdset)
void FD_ISSET(int fd, fd_set *fdset)
宏FD_SET将文件描述符fd添加到文件描述符集fdset中;
宏FD_CLR从文件描述符集fdset中清除文件描述符fd;
宏FD_ZERO清空文件描述符集fdset;
在调用select后使用FD_ISSET来检测文件描述符集fdset中的文件fd发生了变化。
client.c
#include <stdio.h>server_thread.c
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netdb.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
int connect_fd;
int ret;
char snd_buf[1024];
int i;
int port;
int len;
static struct sockaddr_in srv_addr;
//客户端运行需要给出具体的连接地址和端口
if(argc!=3)
{
printf("Usage: %s server_ip_address port\n",argv[0]);
return 1;
}
//获得输入的端口
port=atoi(argv[2]);
//创建套节字用于客户端的连接
connect_fd=socket(PF_INET,SOCK_STREAM,0);
if(connect_fd<0)
{
perror("cannot create communication socket");
return 1;
}
//填充关于服务器的套节字信息
memset(&srv_addr,0,sizeof(srv_addr));
srv_addr.sin_family=AF_INET;
srv_addr.sin_addr.s_addr=inet_addr(argv[1]);
srv_addr.sin_port=htons(port);
//连接指定的服务器
ret=connect(connect_fd,(struct sockaddr *)&srv_addr,sizeof(srv_addr));
if(ret==-1)
{
perror("cannot connect to the server");
close(connect_fd);
return 1;
}
memset(snd_buf,0,1024);
//用户输入信息后,程序将输入的信息通过套接字发送给服务器
//然后调用read函数从服务器中读取发送来的信息
//当输入“@”时,程序退出
while(1)
{
write(STDOUT_FILENO,"input message:",14);
len=read(STDIN_FILENO,snd_buf,1024);
if(len>0)
write(connect_fd,snd_buf,len);
len=read(connect_fd,snd_buf,len);
if(len>0)
printf("Message form server: %s\n",snd_buf);
if(snd_buf[0]=='@')
break;
}
close(connect_fd);
return 0;
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <netdb.h>
#include <pthread.h>
//线程执行函数负责读写
void *thr_fn(void *arg)
{
int size,j;
char recv_buf[1024];
int *parg=(int *)arg;
int new_fd=*parg;
printf("new_fd=%d\n",new_fd);
while((size=read(new_fd,recv_buf,1024))>0)
{
if(recv_buf[0]=='@')
break;
printf("Message from client(%d): %s\n",size,recv_buf);
for(j=0;j<size;j++)
recv_buf[j]=toupper(recv_buf[j]);
write(new_fd,recv_buf,size);
}
close(new_fd);
return 0;
}
int main(int argc,char *argv[])
{
socklen_t clt_addr_len;
int listen_fd;
int com_fd;
int ret;
int i;
static char recv_buf[1024];
int len;
int port;
pthread_t tid;
struct sockaddr_in clt_addr;
struct sockaddr_in srv_addr;
//服务器端运行时要给出端口信息,该端口为监听端口
if(argc!=2)
{
printf("Usage:%s port\n",argv[0]);
return 1;
}
//获得输入的端口
port=atoi(argv[1]);
//创建套接字用于服务器的监听
listen_fd=socket(PF_INET,SOCK_STREAM,0);
if(listen_fd<0)
{
perror("cannot create listening socket");
return 1;
}
//填充关于服务器的套节字信息
memset(&srv_addr,0,sizeof(srv_addr));
srv_addr.sin_family=AF_INET;
srv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
srv_addr.sin_port=htons(port);
//将服务器和套节字绑定
ret=bind(listen_fd,(struct sockaddr *)&srv_addr,sizeof(srv_addr));
if(ret==-1)
{
perror("cannot bind server socket");
close(listen_fd);
return 1;
}
//监听指定端口,连接5个客户端
ret=listen(listen_fd,5);
if(ret==-1)
{
perror("cannot listen the client connect request");
close(listen_fd);
return 1;
}
//对每个连接来的客户端创建一个线程,单独与其进行通信
//首先调用read函数读取客户端发送来的信息
//将其转换成大写后发送回客户端
//当输入“@”时,程序退出
while(1)
{
len=sizeof(clt_addr);
com_fd=accept(listen_fd,(struct sockaddr *)&clt_addr,&len);
if(com_fd<0)
{
if(errno==EINTR)
{
continue;
}
else
{
perror("cannot accept client connect request");
close(listen_fd);
return 1;
}
}
printf("com_fd=%d\n",com_fd);//打印建立连接的客户端产生的套节字
if((pthread_create(&tid,NULL,thr_fn,&com_fd))==-1)
{
perror("pthread_create error");
close(listen_fd);
close(com_fd);
return 1;
}
}
return 0;
}