目录
一,套接字编程基础知识
1, 套接字地址结构
1.1 通用套接字数据结构
struct sockaddr {
sa_family_t sin_family; //协议族
char sa_data[14]; //协议族数据
};
sa_family_t为unsigned short 类型,所以该结构体为16个字节
1.2 实际使用的套接字结构
struct sockaddr_in {
u8 sin_len; //结构体长度,16
u8 sin_family; //通常位AF_INET
u16 sin_port; //端口号
struct in_addr sin_addr; //IP地址
char sin_zero[8]; //未用
};
s
struct in_addr
{
u32 s_addr //32位IP地址,网络字节序
}
由于struct sockaddr和结构struct sockaddr_in的大小是完全一致的,所以进行地址结构设置时,通常的方法是利用结构struct sockaddr_in进行设置,然后强制转换位结构struct sockaddr类型。
二,TCP网络编程流程
1,TCP网络编程架构
1.1 服务器端的程序设计模式
- (socket)套接字初始化过程中,根据用户对套接字的需求来确定套接字选项。这个过程中的函数为socket(),它按照用户定义的网络类型,协议类型和具体的协议标号等参数来定义。系统根据用户的需求生成一个套接字文件描述符供用户使用。
- (bind)套接字与端口的绑定过程中,将套接字与一个地址结构进行绑定。绑定之后,在进行网络程序设计的时候,套接字所代表的IP地址和端口地址,以及协议类型等参数按照绑定值进行操作。
- (listen)由于一个服务器需要满足多个客户端的连接请求,而服务器在某个时间仅能处理有限个数的客户端连接,所以服务器需要设置服务端排队队列的长度,服务器侦听连接 会设置这个参数,限制客户端中等待服务器处理请求的队列长度。
- (accept)在客户端发送连接请求后,服务端需要接受客户端的连接,然后才能进行其他的处理
- (read,write)在服务器接收客户端请求之后,可以从套接字文件描述符中读取数据或者向文件描述符发送数据。接收数据后服务器按照定义的规则对数据进行处理,并将结果发送给客户端
- (close)当服务器处理完数据,要结束与客户端的通信过程的时候,需要关闭套接字连接
1.2 客户端的程序设计模式
客户端程序设计模式流程与服务器的处理模式流程类似,二者的不同之处是客户端在套接字初始化之后可以不进行地址绑定,而是直接连接服务器端
2,创建网络插口函数socket()
该函数建立一个协议族为domain,协议类型为type,协议编号为protocol的套接字文件描述符。
2.1 socket()函数介绍
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
- domain:设置网络通信的域,AF_INET表示IPv4协议
- 返回值:成功返回一个表示套接字的文件描述符。失败返回-1,可以通过errno获得错误原因
- protocol:用于指定某个协议的特定类型,即type类型中的某个类型。通常设置为0
- type:用于设置套接字通信的类型,主要有SOCK_STREAM(流式套接字),SOCK_DGRAM(数据包套接字)
int sock = socket(AF_INET, SOCK_STREAM, 0);
2.2 应用层函数socket()和内核函数之间的关系
用户调用函数 scok = socket(AF_INET, SOCK_STREAM, 0),这个函数会调用系统函数sys_socket(AF_INET, SOCK_STREAM, 0)。系统调用函数sys_socket()分为两部分,一部分生成内核socket结构(注意与应用层的socket()函数是不同的),另一部分与文件描述符绑定,将绑定的文件描述符值传给应用层。
//内核socket结构
struct socket{
socket_state state; // socket 状态
short type ; // socket 类型(SOCK_STREAM等)
unsigned long flags; // socket 标志
struct fasync_struct *fasync_list; // 异步唤醒列表
wait_queue_head_t wait; // 多用户时的等待队列
struct file *file; // 文件指针
struct sock *sock; // socket在网络层的表示;
const struct proto_ops *ops; // 协议特定的socket操作
}
内核函数sock_create()根据用户的domain指定的协议族,创建一个内核socket结构绑定到当前的进程上,其中的type与用户空间用户的设置值是相同的。
sock_map_fd()函数将socket结构与文件描述符列表中的某个文件描述符绑定,之后的操作可以查找文件描述符列表来对应内核socket结构。
3,绑定一个地址端口对bind()函数
该函数将长度为addlen的struct sockadd类型的参数my_addr与sockfd绑定在一起,将sockfd绑定到某个端口上
3.1 bind()函数介绍
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
- sockfd:用socket()函数创建的文件描述符
- my_addr:指向一个结构为sockaddr参数的指针,sockaddr中包含了地址,端口和IP地址的信息。绑定前虚设置好
- addrlen:my_addr结构的长度,可以设置成sizeof(struct sockaddr)
- 返回值:为0时表示成功,-1表示绑定失败
3.2 应用层bind()函数和内核函数之间的关系
应用层的函数bind(sockfd, (struct sockaddr*)&my_addr, sizeof(struct sockaddr))调用系统函数过程sys_bind(sockfd, (struct sockaddr*)&my_addr, sizeof(struct sockaddr))。sys_bind()函数首先调用函数sock_fd_lookup_light来获得文件描述符sockfd对应的内核struct sock结构变量,然后调用函数move_addr_to_kernal()将应用层的参数my_addr复制进内核,放到address变量中。
内核的sock结构是在调用socket()函数时根据协议生成的,它绑定了不同协议族的bind()函数的实现方法,在AF_INET族中的实现函数为inet_bind(),即会调用AF_INET族的bind()函数进行绑定处理
4,监听本地端口listen
函数listen()用来初始化服务器可连接队列,服务器处理客户端连接请求的时候是顺序处理的,同一时间仅能处理一个客户端连接。当多个客户端的连接请求同时到达的时候,服务器并不是同时处理,而是将不能处理的客户端连接请求放到等待队列中,这个队列的长度由listen()函数来定义。
4.1 listen函数介绍
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
- sockfd:调用socket()返回的文件描述符
- backlog:表示在accept()函数处理之前在等待队列中的客户端的长度
- 返回值:成功返回0,失败返回-1,并且设置errno
4.2 应用层listen()函数和内核函数之间的关系
应用层的listen()函数对应于系统调用sys_listen()函数。sys_listen()函数首先调用sockfd_lookup_light()函数获得sockfd对应的内核结构struct socket,查看用户的backlog设置值是否过大,如果过大则设置为系统默认最大设置。然后调用抽象的listen函数,这里指的是AF_INET的listen()函数和inet_listen()函数
5,接受一个网络请求accept函数
当一个客户端的连接请求到达服务器主机侦听的端口时,此时客户端的连接请求会在队列中等待,直到使用服务器处理接收请求。函数执行成功后,会返回一个新的套接字文件描述符来表示客户端的连接,客户端连接的信息可以通过这个新描述符来获得。因此当服务器成功处理客户端的请求连接后,会有两个文件描述符,老的文件描述符表示正在监听的socket,新产生的文件描述符表示客户端的连接,函数send()和recv()通过新的文件描述符进行数据收发。
inet_listen()函数首先判断是否合法的协议族和协议类型,再更新socket的状态值为TCP_LISTEN,然后为客户端的等待队列申请空间并设定侦听端口。
5.1 accept()函数介绍
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd:调用socket()生成的文件描述符
- addr:函数返回的时候,会将客户端的信息存储在参数addr中
- 返回值:成功返回新连接的客户端套接字文件描述符,与客户端之间的通信通过该描述符来进行,失败返回-1.
- addrlen:表示addr所指参数的长度,此参数是一个指针而不是结构
5.2 应用层accept()函数和内核函数之间的关系
应用层的accept()函数对应内核层的sys_accept()函数系统调用函数。函数sys_accept()查找文件描述符对应的内核socket结构,申请一个用于保存客户端连接的新的内核socket结构,获得客户端的地址信息,将连接的客户端地址信息复制到应用层的用户,返回连接客户端socket对应的文件描述符。
6,连接目标网络服务器connect()函数
客户端在建立套接字之后,不需要进行地址绑定就可以直接连接服务器。此函数连接指定参数的服务器,例如IP地址,端口等。
6.1 connect()函数介绍
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd:建立套接字时返回的套接字文件描述符
- addr:指向数据结构sockaddr的指针,其中包括客户端需要连接的服务器的目的端口和IP地址,以及协议类型。
- addrlen:表示第二个参数内容的大小
- 返回值:成功为0,失败为-1.
6.2 应用层connect()函数和内核函数之间的关系
内核层的connect()函数主要进行不同的协议映射的时候要根据协议的类型进行选择,例如数据报和流式数据的connect()函数不同,流式的回调函数为inet_stream_connect(),数据报的回调函数为inet_dgram_connect()
7,写入数据函数write()
当服务器在接收到一个客户端的连接后,可以通过套接字描述符进行数据的写入操作。对套接字进行写入的形式和过程与普通文件的操作方式一致,内核会根据文件描述符的值来查找所对应的属性,当为套接字的时候,会调用相对应的内核函数。
int size;
char data[1024];
size = write(fd, data, 1024);
8,读取数据函数read()
使用read()函数可以从套接字描述符中读取数据。在读取数据之前,必须建立套接字并连接4
int size;
char data[1024];
size = read(fd, data, 1024);
9,关闭套接字函数
9.1 关闭sockeet()连接可以用close()函数实现,函数的作用是关闭已经打开的socket()连接,内核会释放相关的资源,关闭套接字之后就不能再使用这个套接字文件描述符进行读写操作了。
9.2 函数shutdown()可以使用更多方式来关闭连接,允许单方向切断通信或者切断双方的通信
#include
int shutdown(int sockfd, int how);
- sockfd:切断通信的套接口文件描述符
- how:切断的方式
- SHUT_RD:值为0,表示切断读,之后不能使用此文件描述符进行读操作
- SHUT_WR:值为1,表示切断写,之后不能使用此文件描述符进行写操作
- SHUT_RDWR:值为2,表示切断读写,之后不能使用此文件描述符进行读写操作,与close()函数功能相同
- 返回值:成功返回0,失败返回-1
三,服务器/客户端的简单例子
1,例子功能描述
程序分为服务器端和客户端,客户端连接服务器后从标准输入读取输入的字符串,发送给服务器;服务器接收到字符串后,发送接收到的总字符串个数给客户端;客户端将接收到的服务器的信息打印到标准输出
2,服务器网络程序
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 8899
#define BACKLOG 5
void process_conn_server(int socket)
{
ssize_t size = 0;
char buffer[1024];
for(;;)
{
size = read(socket, buffer, 1024);
if(size == 0) //没有数据
{
return;
}
sprintf(buffer, "%ld bytes altogether\n", size);
write(socket, buffer, strlen(buffer)+1); //发给客户端
}
}
int main()
{
int server_socket; //服务器socket描述符
int client_socket; //客户端socket描述符
struct sockaddr_in server_addr; //服务器地址结构
struct sockaddr_in client_addr; //客户端地址结构
int err;
pid_t pid;
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket < 0)
{
printf("socket error\n");
return -1;
}
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET; //协议族
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //本地地址
server_addr.sin_port = htons(PORT); //服务器端口
err = bind(server_socket, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
if (err < 0)
{
printf("bind error\n");
return -1;
}
err = listen(server_socket, BACKLOG);
if (err < 0)
{
printf("listen error\n");
return -1;
}
for(;;)
{
socklen_t addrlen = sizeof(struct sockaddr);
client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &addrlen);
if (client_socket < 0)
{
continue;
}
pid = fork();
if (pid == 0)
{
close(server_socket); //子进程
process_conn_server(client_socket);
}
else
{
close(client_socket); //父进程
}
}
}
3,客户端网络程序
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 8899
void process_conn_client(int sock)
{
ssize_t size = 0;
char buffer[1024];
printf("1");
for (;;)
{
size = read(0, buffer, 1024); //从标准输入读
if (size > 0)
{
write(sock, buffer, size); //发送给服务器
size = read(sock, buffer, 1024); //从服务器读取数据
write(1, buffer, size); //写到标准输出
}
}
}
int main(int argc, char* argv[])
{
int sock;
struct sockaddr_in server_addr;
sock = socket(AF_INET, SOCK_STREAM, 0);
if (socket < 0)
{
printf("socket error\n");
return -1;
}
printf("1");
//设置地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(PORT);
printf("1");
//将用户输入的字符串类型的IP地址转为整形
inet_pton(AF_INET, argv[1], &server_addr.sin_addr.s_addr);
printf("1");
//连接服务器
connect(sock, (struct sockaddr*)&server_addr, sizeof (struct sockaddr));
printf("1");
//客户端处理过程
process_conn_client(sock);
}
启动先启动服务器,再启动客户端,启动客户端需传入IP地址127.0.0.1。