套接字地址结构
进行套接字编程需要指定套接字的地址作为参数。不同的协议族有不同的地址结构定义方式。 这些地址结构通常以sockaddr_开头,每一个协议族有一个唯一的后缀。
通用套接字数据结构
struct sockaddr { //套接字地址结构
sa_family_t sa_family; //协议族 unsigned short类型 长度为16个字节
char sa_data[14]; //协议族数据
}
实际使用的套接字数据结构
因为通用套接字在实际使用中并不方便,比如在以太网中,一般采用struct sockaddr_in
进行设置
struct sockaddr_in { //以太网套接字地址结构
u8 sin_len; //结构struct sockaddr_in的长度
u8 sin_family; //通常为AF_INET
u16 sin_port; //16位的端口号,网络字节序
struct in_addr sin_addr; //IP地址32位
char sin_zero[8]; //未用到
};
socket数据如何在内核中接收和发送
socket数据在内核中的流程主要包括初始化、销毁、接收和发送网络数据。其过程涉及网卡驱动、网络协议栈和应用层的接口函数。
套接字参数中有部分参数是需要用户传入的,这些参数用来与Linux内核进行通信,例如指向地址结构的指针。
向内核传入数据的函数有send(),bind()等,从内核中得到数据的函数有accept(),recv()等。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XUrlB90N-1649165402418)(https://gitee.com/znxcode/drawingbed/raw/master/img/image-20220320160123829.png)]
/*创建套接字并进行bind*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#define MYPORT 3490
int main() {
int sockfd; //套接字文件描述符变量
struct sockaddr_in my_addr; //以太网套接字地址结构
sockfd = socket(AF_INET, SOCK_STREAM,0); //建立套接字文件描述符
//在建立套接字文件描述符后,需要对套接字进行端口和地址的绑定,才能进行数据的接收和发送操作
if (sockfd == -1) { /*检查是否正常初始化 socket*/
perror("socket");
exit(EXIT_FAILURE);
}
my_addr.sin_family = AF_INET; //协议族
my_addr.sin_port = htons(MYPORT); //端口地址
my_addr.sin_addr.s_addr = inet_addr("192.168.232.138"); //将IP地址转化为网络字节序
bzero(&(my_addr.sin_zero), 8); //将my_addr.sin_zero置0
if (bind(sockfd, (struct sockaddr *)&my_addr,
sizeof(struct sockaddr)) == -1) { /*判断是否bind成功*/
perror("bind");
exit(EXIT_FAILURE);
}
close(sockfd); //关闭套接字
return 0;
}
listen():
该函数是用来初始化服务器可连接队列的。因为服务器在处理多个客户端请求时,并不是同时处理,而是将剩余的请求放在请求队列中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6fpBr3mU-1649165402419)(https://gitee.com/znxcode/drawingbed/raw/master/img/image-20220320212013545.png)]
上表为listen()的errno值及其含义
listen函数仅对SOCK_STREAM和SOCK_SEQPACKET的协议有效
if ((listen(sockfd, 5)) == -1) { /*进行侦听绑定*/
perror("listen");
exit(EXIT_FAILURE);
}
以上代码为侦听队列的绑定
accept():
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
通过accept函数可以得到成功连接客户端的IP地址、端口和协议族等信息,这个信息是通过参数addr获得的。当accept()返回的时候,会将客户端的信息存储在参数addr中。需要注意的是,addrlen参数是一个指针而不是结构,accept()将这个指针传给TCP/IP协议栈。
accept()函数的返回值是新连接的客户端套接字文件描述符。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#define MYPORT 3490
int main() {
int sockfd; //套接字文件描述符变量
int client_fd; //客户端套接字文件描述变量
struct sockaddr_in my_addr; //以太网套接字地址结构(本地地址信息)
struct sockaddr_in client_addr; //客户端连接的地址信息
int addr_length; //用于保存网络地址长度
sockfd = socket(AF_INET, SOCK_STREAM,0); //建立套接字文件描述符
//在建立套接字文件描述符后,需要对套接字进行端口和地址的绑定,才能进行数据的接收和发送操作
if (sockfd == -1) { /*检查是否正常初始化 socket*/
perror("socket");
exit(EXIT_FAILURE);
}
my_addr.sin_family = AF_INET; //协议族
my_addr.sin_port = htons(MYPORT); //端口地址
my_addr.sin_addr.s_addr = INADDR_ANY; //自动IP地址获得
bzero(&(my_addr.sin_zero), 8); //将my_addr.sin_zero置0
if (bind(sockfd, (struct sockaddr *)&my_addr,
sizeof(struct sockaddr)) == -1) { /*判断是否bind成功*/
perror("bind");
exit(EXIT_FAILURE);
}
if ((listen(sockfd, 5)) == -1) { /*进行监听绑定*/
perror("listen");
exit(EXIT_FAILURE);
}
return 0;
addr_length = sizeof(struct sockaddr_in); //获取地址长度
client_fd = accept(sockfd, & client_addr, & addr_length); //等待客户端连接
if(client_fd == -1){ /*accept 出错 */
perror("accept");
exit(EXIT_FAILURE);
}
close(client_fd);
close(sockfd); //关闭套接字
}
connect():
客户端在建立套接字之后,不需要进行地址绑定,就可以直接连接服务器。
int connect(int sockfd, struct sockaddr *, int addrlen);
TCP通信例子
server.c
int main(int argc, char *argv[])
{
int ss,sc; //ss为服务器的描述符,sc为服务器的描述符
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
int err;
pid_t pid; //定义进程ID号
ss = socket(AF_INET, SOCK_STREAM, 0); //建立套接字描述符
if(ss < 0){
printf("socket error\n");
return -1;
}
bzero(&server_addr, sizeof(server_addr)); //置0
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(PORT);
err = bind(ss, (struct sockaddr*)&server_addr, sizeof(server_addr)); /*将套接字描述符和
* 套接字结构bind*/
if(err < 0){
printf("bind error\n");
return -1;
}
err = listen(ss, BACKLOG); //创建监听且数量为2
if(err < 0){
printf("listen error\n");
return -1;
}
for(;;) { //开始循环等待客户端连接
int addrlen = sizeof(struct sockaddr);
sc = accept(ss, (struct sockaddr*)&client_addr, &addrlen); //接收客户端连接请求
if(sc < 0){
continue; //请求失败退出该次循环
}
pid = fork(); //分叉进程
if( pid == 0 ){ //判断是否仍然是在等待客户端连接的进程
close(ss);
process_conn_server(sc);
}else{ //
close(sc);
}
}
}
client.c
int main(int argc, char *argv[])
{
int s;
struct sockaddr_in server_addr;
int err;
s = socket(AF_INET, SOCK_STREAM, 0);
if(s < 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);
inet_pton(AF_INET, argv[1], &server_addr.sin_addr); //将用户传入的字符串类型的IP地址转换为整型
connect(s, (struct sockaddr*)&server_addr, sizeof(struct sockaddr)); //连接服务器
process_conn_client(s); //客户端处理过程
close(s);
}
proccess.c
void process_conn_server(int s)
{
ssize_t size = 0;
char buffer[1024]; //数据缓冲区
for(;;){
size = read(s, buffer, 1024); //从套接字中读取放到缓冲区的buffer
if(size == 0){
return;
}
sprintf(buffer, "%d bytes altogether\n", size);
write(s, buffer, strlen(buffer)); //将返回数据发送给客户端
}
}
void process_conn_client(int s)
{
ssize_t size = 0;
char buffer[1024];
for(;;){
size = read(0, buffer, 1024);
if(size > 0){
write(s, buffer, size);
size = read(s, buffer, 1024);
//sprintf(buffer, "%d bytes altogether\n", size);
write(1, buffer, size);
}
}
}
套接字选项
在进行网络编程的时候,经常需要查看或者设置套接字的某些特性。例如设置地址复用、读写数据的超时时间、对读缓冲区的大小进行调整等操作。获得套接字选项设置情况的函数是getsockopt()
,设置套接字选项的函数是setsockopt()
。
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int s, int level, int optname, void *optval, socklen_t
*optlen); //获得套接字选项设置情况
int setsockopt(int s, int level, int optname, const void *optval, socklen_t
optlen); //设置套接字选项
- s:套接字描述符
- level:选项所在协议层
- optname:选项名
- optval:操作的内存缓存区
- oplen:上面参数的长度