1.Socket(套接字)
是一种通讯机制,它包含一整套的调用接口和数据结构的定义,它给应用进程提供了使用如TCP/UDP等网络协议进行网络通讯的手段。
Linux中的网络编程通过Socket接口实现,Socket即是一种特殊的IO,提供对应的文件描述符 ,(有了文件描述符,就可以读写数据)
一个完整的Socket都有一个相关描述
{协议,本地地址,本地端口,远程地址,远程端口};(五元组)
采用的协议类型,是UDP还是TCP
本机地址和端口
远程地址和端口
每一个Socket有一个本地的唯一的Socket,由操作系统分配。
Socket有两种,一种是服务器端的Socket,另一种是客户端的Socket(可以有系统自动分配)
2.创建Socket
#include <sys/socket.h>
int socket(int domain, int type, int protocol) domain因特网域
返回文件描述符,利用这个文件描述符,就可以进行读写操作,在网络中进行数据的传输,传输给对方,就写入信息,要读取信息,就read
参数因特网域
AF_INET IPV4因特网域 AF_INET6 IPV6域 protocol通常为0,表示按给定的域和套接字类型选择默认协议
第二个参数
1.SOCK_STREAM 采用TCP协议
2.SOCK_DGRAM 使用数据报协议UDP协议
3.SOCK_RAW 原始套接字允许对低层协议如IP或ICMP直接访问,主要用于新的网络协议实现的测试
4.SOCK_SEQPACKET 长度固定,有序,可靠的面向链接报文传递
Socket创建在内核当中 有个Socket结构体
3.Socket是内核给用户提供的接口,内核有Socket相关结构体和接口函数
4.字节序
4.1大端字节序,多字节数据的最高有效字节(8位)在最低的内存地址中
4.2小端字节序 则和大端字节序相反
most significant byte first (多字节数据高位在前)
Least significant byte first (低位在前)
4.3网络协议使用网络字节序,就是大端字节序
4.4字节序转换函数
网络传输数据采用大端字节序 ,是统一的,所以对于内部字节表示顺序 和网络字节顺序不同 的机器,就一定要对数据进行转换
字节序转换函数如下:
1.主机字节序是本机采用的字节序,不管主机采用什么字节序,统一转换为大端字节序
unit32_t htonl(uint32_t hostlong);将一个32位整数由主机字节序转换为网络字节序
uint16_t htons(uint16_t hostshort); 将一个16位整数由主机字节序转换为网络字节序
uint32_t ntohl(uint32_t netlong); 将一个32位整数由网络字节序转换为主机字节序
uint16_t ntohs(uint16_t netshort); 将一个16位整数由网络字节序转换为主机字节序
4.5通用地址结构
sa_family 采用的IPV4还是IPV6
sa_data包含一些远程电脑的地址,端口和套接字的数目,它里面的数据是杂溶在一起的
4.6因特网地址结构体sockaddr_in (编程中专用的因特网的结构)
sin_addr要用网络字节序 实际上,使用因特网地址结构在调用函数时要强转成通用地址结构
4.7 IP地址点分十进制和网络字节序之间的转换
#include <arp/inet.h>
addr 就是代转的32位网络字节序的IPV4地址
str:待转换的点分十进制的地址字符串
- const char *inet_ntop(int domain, const void *restrict addr, char* restrict str, sockleb_t size);
网络字节序转换为点分十进制 成功返回地址字符串指针,出错返回NULL
- int inet_pton(int domain, const char *restrict str, void *restrict addr);
点分十进制转换为网络字节序 成功返回1,无效格式返回0, 出错返回-1
4.8填写IPV4地址族结构案例
cpp
struct sockaddr_in sin; //定义一个sockaddr_in结构体
char buf[16];
memset(&sin, 0, sizeof(sin)); //内存清零是个好习惯
sin.sin_family = AF_INET;// 填写Internet地址族,IPV4
sin.sin_port = htons((short)3001); //填写端口号,将16位主机字节序转换为网络字节序
//点分十进制的IP地址转换为网络字节的IP
//填充sin_addr
//第二个参数是要连接的服务器端的IP地址,代转的点分十进制的字符串的IP
//第三个参数是最终放置的网络字节序IP,这个网络字节序IP是0101的形式,用于在网络中传播的
if(inet_pton(AF_INET,"192.168.2.1", &sin.sin_addr.s_addr) <= 0)
{
//错误处理
}
//inet_ntop()网络字节序转换为点分十进制字节序,
//第二个参数是网络字节序IP(0101形式)
//第三个参数存放最终转换后的点分十进制的IP
//最后个是buf的大小
//网络字节序转点分十进制字节序返回值是点分十进制字符串的地址
printf("%s\n", inet_ntop(AF_INET, &sin.sin_addr.s_addr,buf,sizeof(buf)));
5.TCP客户端服务器编程模型
1.如何进行TCP,UDP的编程
1.1服务器端
服务端调用序列步骤:
cpp
1.使用socket()函数创建套接字
2.调用bind()绑定本地地址和端口
3.调用listen()启动监听
4.调用accept()从已连接队列中提取客户连接,队列中没有客户端会阻塞
5.调用I/O函数(read/write)与客户端通讯
6.调用close关闭套接字(close(fd);)
2.客户端:
cpp
1.创建socket()套接字
2.调用connect()连接服务器端,如果能够正确指定服务器端的IP地址和端口,就能连接上来
3.就能够调用I/O函数(read/write)与服务器端通讯
4.通讯完后就可以调用close关闭套接字
6.套接字和地址绑定
cpp
#include <sys/socket.h>
//绑定地址
//第二个参数结构体探入要绑定的IP和端口传入,
//第三个是传入结构体长度
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
成功返回0,出错返回-1
//查找绑定到套接字的地址
//第二个参数是存储的绑定的套接字的地址
//第三个参数是第二个参数的长度,大小
int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
//成功返回0,出错返回-1
//获取对方地址
//peer对等的,同级的
//第二个参数是出传入的结构体指针地址,可以获得对方的地址信息
//第三个是第二个参数的大小
int getpeeername(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
成功返回0,出错返回-1
7.建立连接
1.服务器端
listen()监听(指定port监听) 服务器端里面维护了个队列,将接收到的客户端请求
放置到对应的队列中
第二个参数backlog,指定客户端排队队列长度,自己指定
accept()从已连接的队列中提取用户客户连接
第二个参数指定客户端的相关地址信息可以存放到这里面去,如果不需要,可以为NULL
2.客户端
connect();//获得服务器端的连接
8.先写一个服务器端TCP的程序,然后利用客户端对其进行连接
1.建立服务器端,当客户端连接上来,返回一个服务器端的系统时间
2.bind()
一台主机可以有多个网络接口和多个IP地址,如果我们只关心某个地址的连接请求,我们可以指定一个具体的本地IP地址,如果要响应所有接口上的连接请求就要使用一个特殊的地址
#define INADDR_ANY uint32_t(0x00000000) //8个0
3.出现几种套接字描述符
1.服务器端socket()的套接字描述符
2.accept()返回的描述符
3.客户端的套接字描述符。
但服务端和客户端进行通讯是用的服务器端和客户端的套接字描述符,类似管道一样进行通讯
4.服务器端代码,客户器端连接会收到服务器发来的实时时间
cpp
#include <stdio.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <sys/types.h>
#include <arpa/inet.h>
//这是一个服务器端
//客户端连接上来返回一个系统时间给客户端
int sockfd; //服务器端套接字描述符
//处理ctrl+c终止服务器函数
void sig_handler(int signo)
{
if(signo == SIGINT){
printf("server close\n");
/*步骤6,关闭sockt*/
close(sockfd);
exit(0);
}
}
//输出连接上来的客户端的相关信息
void out_addr(struct sockaddr_in *client)
{
//获得端口,是个网络字节序(转成主机字节序)
int port = ntohs(client->sin_port);
char ip[16];//3*4+3个点+一个结束字符=16
memset(ip, 0, sizeof(ip));
//将IP地址从网络字节序转换成点分十进制
inet_ntop(AF_INET,
&client->sin_addr.s_addr, ip, sizeof(ip));
printf("client: %s(%d) connected\n", ip, port);
}
void do_service(int fd)
{
/*返回系统时间*/
//time返回1970年经过的时间戳
long t = time(0);
//将时间戳转换为时间字符串
char *s = ctime(&t);
size_t size = strlen(s) * sizeof(char);
//将服务器端获得的系统时间写回到客户端
//向fd写入char* s,写入字节数,返回成功写入字节数
if(write(fd, s, size) != size){
perror("write error");
}
}
int main(int argc, char *argv[])
{
//服务器端指定端口,要监听,命令行中传递进去
if(argc < 2){
printf("usage: %s #port\n", argv[0]);
exit(1);
}
//登记一个信号SIGINT,ctrl+c终止服务器端
if(signal(SIGINT, sig_handler) == SIG_ERR)
{
perror("signal sigint error");
exit(1);
}
/*步骤1,创建socket(套接字),
socket创建在内核中,是一个结构体
AF_INET:IPV4
SOCK_STREAM:tcp协议 最后参数0
*/
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
/*步骤2:调用bind将socket
(包括ip,port)进行绑定*/
//sockaddr_in,因特网的专用地址,sockaddr是通用地址
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));//清空
//往地址中填入ip,port,internet地址族类型
serveraddr.sin_family = AF_INET;
//端口号需要为16位网络字节序,atoi()将字符串转换为int
serveraddr.sin_port = htons(atoi(argv[1]));
//网络字节序IP地址,INADDR_ANY响应所有网卡的请求
//一台主机有多个网卡,多个IP地址,
//响应所有网卡来源上的客户端请求
serveraddr.sin_addr.s_addr = INADDR_ANY;
//绑定,第二个参数需要强转为struct sockaddr*
if(bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) < 0){
perror("bind error");
exit(1);
}
/*步骤3,调用listen函数启动
监听(指定port端口监听)
通知系统去接受来自客户端的连接请求
,将接收到的客户端连接放在队列中*/
//backlog,指定客户端排队队列长度
if(listen(sockfd, 10) < 0)
{
perror("listen error");
exit(1);
}
/*步骤4,获得某一个客户端的连接
//调用accept函数从队列中获得一个
客户端的请求连接,并返回新的socket描述符,
在内部会新创建socket返回描述符,通过此描述符和客户端描述符进行通信
//若没有客户端连接,调用accept()会阻塞,直到获得一个客户端连接并返回新的socket描述符
第二个参数用于接收客户端地址信息*/
struct sockaddr_in clientaddr;
socklen_t clientaddr_len = sizeof(clientaddr);
while(1){
int fd = accept(sockfd,
(struct sockaddr*)&clientaddr, &clientaddr_len);
if(fd < 0){
perror("accept error");
continue;//结束本次循环,继续下一次
}
/*步骤5,调用IO函数(read/write)
和连接的客户端进行双向的通讯*/
//输出接收到的客户端地址
out_addr(&clientaddr);
//do_service()和客户端进行通讯服务,传入的是服务端
//accept()获得连接请求返回的套接字描述符
//用于和客户端连接
do_service(fd);
//步骤6,关闭针对客户端的socket,
close(fd);
}
return 0;
}
运行结果:
我按ctrl+c终止服务器了,运行成功
Kotlin
ji@ji-VM:~/c/linux_netprograming$ gcc -o bin/time_tcp_server src/time_tcp_server.c
ji@ji-VM:~/c/linux_netprograming$ ./bin/time_tcp_server 8888
^Cserver close
ji@ji-VM:~/c/linux_netprograming$
5.客户端代码
1.使用Telnet远程登陆(位于应用层),这里数据前加的是Telnet头部,具体在应用层用的那种协议,就在数据前加那种头部,在传到传输层(TCP),进行数据封装
先运行服务器端(是基于TCP的,在传输层):./bin/time_tcp_server 8888
在ubuntu中输入命令:telnet 127.0.0.1 8888 //127.0.0.1只能在本机上测试,但在网络中ip地址是动态分配的
指定服务器IP地址和端口
在ubuntu的火绒浏览器:http://192.168.101.50:8888 回车
应用层开发的软件可以是基于传输层的,所以可以通过网络与tcp服务器通讯
9.编写一个TCP客户端程序,和服务器端TCP程序进行通讯
直接在传输层进行相互通讯
1.创建套接字
2.客户端调用connect函数连接到服务器端,因特网专用结构体,往sockaddr中填入ip.port和地址族类型ipv4
3.与服务端通过read和write通讯
4.关闭sockfd
cpp
#include <netdb.h>
#include <sys/socket.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <arpa/inet.h>
//TCP客户端
int sockfd;
//处理ctrl+c终止客户端函数
void sig_handler(int signo)
{
if(signo == SIGINT){
printf("server close\n");
/*步骤6,关闭sockt*/
close(sockfd);
exit(0);
}
}
int main(int argc, char *argv[])
{
if(argc < 3){
printf("usage: %s ip port\n", argv[0]);
exit(1);
}
if(signal(SIGINT, sig_handler) == SIG_ERR){
perror("signal sigint error");
exit(1);
}
/*步骤1:创建socket*/
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0){
perror("socket error");
exit(1);
}
//因特网专用结构体
//往sockaddr中填入ip.port和地址族类型ipv4
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET; //IPV4
serveraddr.sin_port = htons(atoi(argv[2]));
//将点分十进制的ip地址转换为网络字节序
//第二个参数是输入的字符串形式ip地址,转换到第三个参数(网络字节序)
//第三个参数是serceraddr.sin_addr.s_addr的地址
int res = inet_pton(AF_INET, argv[1],
&serveraddr.sin_addr.s_addr);
if(res < 0){
perror("inet_pton error");
exit(1);
}
/*步骤2:客户端调用connect函数连接到服务器端*/
if(connect(sockfd, (struct sockaddr*)&serveraddr,
sizeof(serveraddr)) < 0){
printf("connect error");
exit(1);
}
/*步骤3:调用IO函数(read/write)和服务器端进行双向通信*/
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
size_t size;
//从套接字描述符读取数据
if((size = read(sockfd, buffer,
sizeof(buffer))) < 0){
perror("read error");
}
//在屏幕上输出read到buffer里面的内容
if(write(STDOUT_FILENO, buffer, size) != size){
perror("write error");
}
/*步骤4:关闭socket*/
close(sockfd);
return 0;
}