Linux网络编程笔记:socket编程(一)
一、概念
1.1简述
socket是一种IPC方法,它允许位于同一主机或使用网络连接起来的不同主机上的应用程序之间交换数据。
socket接口是TCP/IP网络的API,通过此接口,可以开发TCP/IP网络上的应用程序。socket是一种特殊的I/O,也是一种文件描述符。调用类似于打开文件的函数打开socket,会返回一个socket描述符。通过此文件描述符,我们可以建立连接,进行数据传输等操作。
1.2组成
socket实际上可以看成由文件描述符、读缓冲区以及写缓冲区构成。两个socket之间的通信是依靠使用文件描述符,来指定数据的传输。
1.3通信模型中的位置
在四层网络模型中,并没有发现socket的身影。socket接口是不属于四层网络模型中,它是位于应用层与传输层之间,建立两层之间的通信。
二、socket编程
本段内容主要是编写一个服务器与一个客户端之间通信模型,来熟悉socket的使用。
通信过程是最简单的模型,首先是客户端与服务器之间建立连接,后客户端与服务器通信,最后两者断开连接。以下是,客户端与服务器通信流程图。
下面将详细介绍socket编程用到的API函数,以及一些使用注意事项。
2.1socket函数
#include <sys/socket.h>
int socket(int domain, int type, int protocol); //创建套接字
参数:
domain:AF_INET、AF_INET6 //ip版本AF_INET:IPv4 ;AF_INET6:IPv6
type:SOCK_STREAM,SOCK_DGRAM //协议 SOCK_STREAM: TCP; SOCK_DGRAM: UDP
protocol:0 //一般取0
返回值: 成功:新套接字的文件描述符 ;失败:-1
2.2bind函数
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen); //给socket绑定(IP + port)
参数:
sockfd:创建的套接字的文件描述符
addr: (sockaddr*)&ip_addr; //服务器的IP+PORT struct sockaddr_in ip_addr;
addrlen sizeof(ip_addr);
返回值:成功 0,失败-1
在整个网络通信中,需要绑定IP+PORT,只有绑定了,才能找到建立通信的目标计算机的进程。
IP地址:可以在网络环境中,唯一标识一台主机
端口号:可以在一台主机上,唯一识别一个进程
IP地址+端口号:可以在网络环境中,唯一识别一个进程
2.2.1sockaddr结构体
在bind函数的第二个参数中需要传入服务器的IP+PORT,是以结构体的形式传入。随着版本的更新,现在主要使用的是sockaddr_in结构体,然后通过类型转换,传给函数。
sockaddr_in结构体
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
初始化sockaddr_in结构体
struct sockaddr_in addr; //定义结构体
//IP版本
addr.sin_family = AF_INET;
//端口号
addr.sin_port = htons(5556); //htons本地转换为网络,下一小节介绍函数
//ip地址,方法一
addr.sin_addr.s_addr = htonl(ADDRIN_ANY); //通常这样用,ADDRIN_ANY取出本地系统中任意有效的ip地址
//ip地址,方法二
int dst;
inet_pton(AF_INET,"192.168.1.100",(void*)&dst); //inet_pton本地字节序转换为网络字节序
addr.sin_addr.s_addr = dst;
2.2.2网络字节序
网络字节序:
小端:高位存在高地址,地位存在低地址 ;计算机本地采用小端字节序
大端:高位存在低地址,地位存在高地址;TCP/IP协议规定,网络数据采用大端字节序
由于本地字节序与网络的字节序不同,所以在通信是需要进行转换。
网络到计算机需要字节序转换:
htonl() 本地----> 网络 (IP)
htons() 本地----> 网络 (port)
ntohl() 网络----> 本地 (IP)
ntons() 网络----> 本地 (port)
IP地址转换
192.168.1.100 这样IP的格式为十进制的字符串,使用时要转换为int
atoi htonl
192.168.1.100 ------> int -------> 网络
以上步骤封装为一个函数:inet_pton() 还有inet_ntop()
int inet_pton(int af, const char *src, void *dst); 本地----> 网络
af:AF_INET,AF_INET6 IP版本号
src:传入IP地址(点分十进制)
dst:传出IP地址
返回值:成功1;
异常0,说明src指向异常ip地址;
失败:-1
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size); 网络----> 本地
af:AF_INET,AF_INET6 IP版本号
src:网络字节序ip地址
dst:本地ip
size:dst的大小
返回值: 成功 dst
失败NULL
2.3listen函数
int listen(int sockfd, int backlog); //设置与服务器来连接最大数
参数:
sockfd:创建的套接字的文件描述符
backlog:最大数量
返回值:成功 0,失败-1
2.4accept函数
int accept(int sockfd, struct sockaddr *addr,
socklen_t *addrlen); //阻塞等待客户端连接,成功连接的话,返回一个与客户端连接成功的socket文件描述符
参数:
sockfd:创建的套接字的文件描述符
addr:传出参数。返回成功连接客户端的地址结构(ip+port)
addrlen:传入传出参数 &client_addr_len
传入:addr大小;传出:客户端addr实际大小
socklen_t client_addr_len = sizeof(addr);
返回值:
成功:能与客户端通信的socket对应的文件描述符(服务器第二个套接字,accept产生的)
失败:-1
2.5connect函数:
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);//使用现有的socket,与服务器建立连接(客户端自己的socket)
参数:
sockfd:套接字的文件描述符(客户端自己的socket)
addr:服务器的地址结构
addrlen:sizeof(addr);
返回值:成功 0,失败-1
三、socket的应用
本章将前面介绍的几个有关socket的函数应用到TCP通信中,完成单个客户端与服务器简单数据传输的例程。
编写的依据是第二章开始的客户端服务器通信流程。
3.1服务器端程序流程
1.socket() //创建套接字
2.bind() //绑定地址结构
3.listen() //设置监听上线
4.accept() //等待连接
5.read(fd) //读数据
6.操作数据
7.write(fd)
8.close(fd)
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/stat.h>
#include<string.h>
#include <sys/socket.h>
#include<arpa/inet.h>
#include<ctype.h>
#define MAX_LISTEN_NUM 5
int main(void){
int lfd ,cfd;
int ret,buf_size;
struct sockaddr_in server_addr,client_addr;
socklen_t client_addr_len;
char buf[1024],client_IP[1024];
lfd = socket(AF_INET, SOCK_STREAM, 0); //创建socket
if(lfd == -1){
printf("socket failed\n");
exit(1);
}
server_addr.sin_family = AF_INET; //设置服务器IP+PORT
server_addr.sin_port = htons(5678); //设置端口号,任意设定,要大于5000
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //自动获取自己的IP
ret = bind(lfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); //绑定
if(ret != 0){
printf("bind failed\n");
exit(1);
}
ret = listen(lfd, MAX_LISTEN_NUM); //设置监听数
if(ret != 0){
printf("listen failed\n");
exit(1);
}
client_addr_len = sizeof(client_addr);
cfd = accept(lfd, (struct sockaddr *)&client_addr, &client_addr_len); //等待连接
if(cfd == -1){
printf("accept failed\n");
exit(1);
}
printf("client ip:%s port:%d\n",
inet_ntop(AF_INET,&client_addr.sin_addr.s_addr,client_IP,sizeof(client_IP)),
ntohs(client_addr.sin_port));//打印连接的客户端IP+PORT
while(1){
buf_size = read(cfd, buf, sizeof(buf)); //从客户端读数据
if(buf_size == -1){
printf("read failed\n");
exit(1);
}
write(STDOUT_FILENO, buf,buf_size); //将数据打印到终端
}
return 0;
}
3.2客户端流程
1.socket() //创建套接字
2.connect() //连接服务器
3.write(fd) //写数据给服务器
4.read(fd) //读数据
5.close
客户端程序自己可以不使用bind绑定客户端地址结构,计算机会自动采用隐式绑定
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/stat.h>
#include<string.h>
#include <sys/socket.h>
#include<arpa/inet.h>
#include<ctype.h>
int main(void){
int lfd ;
int ret,buf_size;
struct sockaddr_in server_addr;
socklen_t client_addr_len;
char buf[1024];
lfd = socket(AF_INET, SOCK_STREAM, 0); //创建socket
if(lfd == -1){
printf("socket failed\n");
exit(1);
}
server_addr.sin_family = AF_INET; //设置服务器IP+PORT
server_addr.sin_port = htons(5678); //服务器的端口号,与服务器程序中一致
inet_pton(AF_INET,"192.168.138.130",&server_addr.sin_addr.s_addr);//服务器端的IP地址
ret = connect(lfd, (struct sockaddr *)&server_addr, sizeof(server_addr));//使用现有的socket,与服务器建立连接(客户端自己的socket)
if(ret != 0){
printf("connect failed\n");
exit(1);
}
while(fgets(buf, 1024, stdin) != NULL){ //循环写
write(lfd,buf,strlen(buf));
memset(buf, 0, 1024);
}
close(lfd);
return 0;
}
3.3通讯结果
开启两个终端,分别编译运行服务器与客户端的程序,然后在客户端输入数据,服务器接受数据,并把数据打印到终端。
客户端:输入数据
服务器端:读取数据
实验环境在同一台电脑上,所以客户端服的IP地址与设置的服务器地址一样,但客户端的端口是随机使用的。
四、总结
以上重点介绍了几个关于socket编程的函数,并且实现了简单的服务器与客户端的通信。但是,以上的例程只是实现了一个客户端去访问服务器,在现实生活中是不能满足需求的。要求多个客户端能够同时访问服务器进行数据传输,也就是要求并发实现。具体内容参考下篇。