在学习Linux程序设计时,学会网络编程是必不可少的,而Linux网络编程一般通过socket(套接字)接口实现。在学习socket编程之前,我们首先要了解相关网络协议。
Linux中继续使用TCP/IP的网络层结构,即从下到上分为物理层,数据链路层,网络层,传输层 ,会话层,表示层,应用层。其中,传输层在实现通信服务时发挥了重要作用。传输层定义了TCP以及UDP协议。TCP是面向连接的通信协议,提供可靠的数据传送。TCP将源主机应用层的数据分为多个分段,将每个分段传输到网际层,网际层将数据封装为IP数据包,并发送到目的主机。
网络结构模式分为两种:客户机/服务器模式以及浏览器/服务器模式,在这里主要围绕客户机/服务器模式展开。一个完整的TCP程序设计流程如下:服务器调用socket,bind,listen函数完成初始化,接着调用accept函数阻塞等待,此时服务器处于监听端口的状态。客户端调用socket函数进行初始化,接着调用connect函数发出SYN段并阻塞,等待服务器应答。服务器应答一个SYN-ACK段,客户端收到后从connect函数返回,同时应答一个ACK段,服务器收到后从accept函数返回。此时,连接已经成功建立,可以开始传输数据了。一般由客户端发出请求,服务器处理请求,进行一问一答的方式。所以,服务器从accept函数返回后,调用read函数读取客户端的请求,如果没有请求就阻塞等待。客户端调用write函数发送请求,服务器收到请求后从read函数返回,对客户端的请求进行处理,在此期间服务器段接着调用read函数阻塞等待客户端的下一条请求。客户端,服务器之间发送,传输数据可以用send,recv函数实现。最后,数据传输完毕,客户端调用close函数关闭连接,服务器的read函数就会返回0,服务器就知道客户端关闭了连接,也调用close函数关闭连接。
套接字是一种通信机制,用于描述IP地址和端口,是一种特殊的I/O。我们常用流式套接字,即SOCK_STREAM,它提供了一个可靠的,面向连接的数据传输服务,数据无差错无重复地发送且按发送顺序接受。
int socket(int domain, int type, int protocol) #用于创建一个新的套接字
#domain又称为协议族,决定了套接字的地址类型以及在通信中必须采用对应的地址,常用套接字有AF_INET,AF_INET6…AF_INET决定了要用IPv4地址(32位)与端口号(16位)的组合。
#type指定了套接字类型,常用的套接字类型有SOCK_STREAM,SOCK_DGROM等
#protocol用于指定协议,常用的协议有:IPPROTO_TCP(TCP协议),IPPTOTO_UDP(UDP协议)等,一般置为0,表示使用默认协议。
int bind(int sockfd, const struct sockaddr * addr, socklen_t addrlen) #将一个套接字绑定到一个地址上,使得客户端可以连接
#sockfd:上一个socket调用中获得的文件描述符
#addr指向包含绑定地址的结构的指针
#addrlen指的是地址结构的大小
一个struct sockaddr_in 结构如下:
struct sockaddr_in servaddr; //结构体定义
bzero(&servaddr, sizeof(servaddr)); //结构体清零
servaddr.sin_family = AF_INET; //设置地址类型为AF_INET
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//设置网络地址为INADDR_ANY
servaddr.sin_port = htons(6666); //设置端口号为6666
int listen(int sockfd, int backlog)
#sockfd:socket调用中获得的文件描述符
#backlog:sockfd挂起的已连接队列可以增长的最大长度
int accept(int sockfd, struct sockaddr * addr, socklen_t * addrlen) #建立客户端程序对该套接字的连接
#sockfd:socket调用中获得的文件描述符
#addr:指向连接对端的地址,即客户端地址
#addrlen指定地址结构的大小
int connect(int sockfd, const struct sockaddr * addr, socklen_t addrlen) #用于客户端建立连接,发起三次握手过程。如果连接不能立刻建立起来,connect调用会阻塞一段不确定的倒计时时间,这段倒计时时间结束后这次连接就会失败。
#sockfd:socket调用中获得的文件描述符
#addr指向服务器地址
#addrlen指定地址结构的大小
int send(int sockfd, const void * buf, size_t len, int flags) #用于发送数据
#sockfd:socket调用中获得的文件描述符
#buf:指向发送数据的指针
#len:数据的长度
#flags:一般置为0
int recv(int sockfd, const void * buf, size_t len, int flags)
#sockfd:socket调用中获得的文件描述符
#buf:要读取信息的缓冲
#len:缓冲的最大长度
#flags:一般置为0
int close(int fd) #释放系统分配给套接字的资源,关闭连接
#fd:文件描述符,在socket编程中指sockfd
在Linux中可以使用int inet_pton(int af, const char *src, void *dst)来转换IP地址。
一个C/S模型如下:
//服务器端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/fcntl.h>
#include <netdb.h>
#include <strings.h>
#define SERV_PORT 6666
#define BUF_SIZE 5000
#define FILE_NAME_MAX_SIZE 500
void fatal(char *string) {
printf("%s\n",string);
exit(1);
}
int main()
{
struct sockaddr_in servaddr, cliaddr; //定义服务器与客户端地址结构体
socklen_t cliaddr_len; //客户端地址长度
int listenfd, connfd, sa, fd, bytes;
char buf[BUF_SIZE];
char str[INET_ADDRSTRLEN];
int i, n;
//创建服务器端套接字文件
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));
//监听,并设置最大连接数为20
listen(listenfd, 20);
printf("Accepting connections ...\n");
while (1) {
sa = accept(listenfd, 0, 0);
if (sa < 0)
fatal("accept failed");
recv(sa, buf, BUF_SIZE, 0);
fd = open(buf, O_RDONLY);
if (fd < 0)
fatal("open failed");
while (1) {
bytes = read(fd, buf, BUF_SIZE);
if (bytes <= 0)
break;
write(sa, buf, bytes);
}
close(fd);
close(sa);
}
return 0;
}
//客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/fcntl.h>
#include <netdb.h>
#include <strings.h>
#define SERV_PORT 6666
#define BUF_SIZE 5000
#define PATH " " //定义文件位置
#define DEST_IP "127.0.0.1" //IP地址设置为本机
void fatal(char *s) { //用于传参
printf("%s\n",s);
exit(1);
}
int main()
{
int listenfd, c, bytes;
char buf[BUF_SIZE];
struct sockaddr_in cliaddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0); //建立socket连接
if (listenfd < 0)
fatal("socket");
bzero(&cliaddr, sizeof(cliaddr)); //定义结构体
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(SERV_PORT);
if (inet_aton(DEST_IP, &cliaddr.sin_addr) == 0) { //IP地址的转化
fprintf(stderr, "Inet_aton erro\n");
exit(1);
}
c = connect(listenfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
if (c < 0)
fatal("connect failed");
send(listenfd, PATH, strlen(PATH), 0);
while (1) {
bytes = read(listenfd, buf, BUF_SIZE);
if(bytes <= 0)
exit(0);
write(1, buf, bytes);
}
return 0;
}
以上程序实现了客户端与服务器建立连接,客户端请求一个文档并显示。