【Linux】socket网络编程基础知识
前言
————————————————
版权声明:本文为优快云博主「SogK1997」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/dive668/article/details/119164392
————————————————
本博文是对大作业的预热,此次大作业涉及到Linux课程第十章:网络编程的相关知识,因此着重以老师PPT上的内容为要,进行代码编写。以C/C++为主。
argc !=2是什么意思
What does argc mean? [duplicate]
argc是参数的数量,并且argv是一个字符串数组。
程序本身是第一个参数argv[0],因此argc总是至少为 1。
那么,argc是2当程序使用一个命令行参数运行。如果它不带参数运行,或者超过一个参数,argc != 2则为真,因此将打印使用消息“Usage: display_image ImageToLoadAndDisplay”,告诉用户如何正确运行它。
inet pton函数
对stdin,stdout 和STDOUT_FILENO,STDIN_FILENO的学习
对stdin,stdout 和STDOUT_FILENO,STDIN_FILENO的学习
基础知识
10.1 网络协议与体系结构
五层协议体系结构中各层的功能分别如下:
- 应⽤层为应用进程提供服务,定义了应⽤进程间通信和交互的规则。
- 传输层为应用进程提供连接服务,实现连接两端进程的会话。
- 网络层为分组交换网上的不同主机提供通信服务。
- 数据链路层可简称为链路层,该层将从网络层获取的IP数据报组装成帧,在网络结点之间以帧为单位传输数据。
- 物理层以比特为单位传输数据,该层定义了与网络相关的硬件的规范,如表示“0”、“1”电压的电压数值、硬件连接方式等。
协议通常都由如下几个部分组成:
(1)待交互数据的结构和格式;
(2)进行交互的方式,包括数据的类型、对数据的处理动作等;
(3)事件实现顺序的说明。
体系结构中各层的实现建⽴在其下⼀层所提供的服务上,并向其上层提供服务
10.2 socket编程基础
socket本意为“插座”,常被称为套接字,当使用socket进行通讯时,进程会先生成一个socket⽂件,之后再通过socket⽂件进行数据传递。
在TCP/IP协议族中,使用IP地址和端口号可以唯一标识网络中的⼀个进程。
socket接口位于应⽤层与TCP/IP协议族之间,是基于软件的抽象层。
Linux系统中常用的socket网络编程接口有socket()、bind()、listen()、accept()、connect()、send()、recv()、close()
,其中
connect
为客户端专用接口,bind()
、listen()
、accept()
为服务器端专用接口,socket()
与close()
则由服务器与客户端共用。
10.3 socket编程接口
socket()
socket()函数存在于函数库sys/socket.h
中,其声明如下:
int socket(int domain, int type, int protocol);
socket()函数用于创建套接字,也可以说socket()
函数用于打开网络通讯端口,该函数类似于文件操作中的open()
函数,若调用成功,也返回⼀个文件描述符,之后应用程序可以采用socket通信中的读写函数在网络中收发数据;若调用失败会返回-1,并设置errno。
参数domain
用于选择通信时的协议族。常网设置为:
AF_INET
:⽹络通信AF_UNIX
:本地通信
这些协议族都在头文件sys/socket.h
中定义。
参数type
用于指定socket的连接类型,其常用取值分别为:
- SOCK_STREAM:TCP协议
- SOCK_DGRAM:UDP协议
- SOCK_RAW:ICMP协议
参数protocol
⼀般设置为0,表示使用默认协议。
bind()
bind()函数存在于函数库sys/socket.h
中,其声明如下:
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
bind()函数的功能为:使服务器端的一个socket⽂件与网络中的⼀个进程进行绑定,因为文件描述符可标识socket⽂件,“主机名+端口号”可标识网络中的唯⼀进程,因此bind()函数实际上是将服务器端的socket⽂件与网络中的进程地址进行绑定。
参数sockfd
指代socket文件的文件描述符,⼀般由socket()函数返回;
参数addr
指代需要与服务器进行通信进程的地址,其本质为struct sockaddr
结构体类型的指针,struct sockaddr
结构体的类型定义如下:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14]; //进程地址
}
addrlen
表示参数addr
的长度,实质上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(85); //设置端⼜号为85
listen()
listen()
函数存在于函数库sys/socket.h
中,其声明如下:
int listen(int sockfd, int backlog);
listen()
函数仍用于服务器端,从字面上看,其功能为使已绑定的socket监听对应客户端进程状态,但实际上,该函数用于设置服务器同时可建立的连接的数量。
- 参数
sockfd
表示socket⽂件描述符 - 参数
backlog
用于设置请求队列的最大长度 listen()
函数若调用成功则返回0
,表示监听成功;否则返回-1
,并设置errno
。
accept()
accept()
函数存在于函数库sys/socket.h
中,其声明如下:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept()
函数在listen()
函数之后使用,其功能为阻塞等待客户端的连接请求。
当传输层使用TCP协议时,服务器与客户端在创建连接前,会先经过三次握手机制测试连接,三次握手完成后,服务器调用accept()
函数处理连接请求,此时若还没有客户端的请求到达,便阻塞等待调用accept()函数的进程,直到接收到客户端发来的请求,且服务器中已创建的连接数未达到backlog
,accept()
函数才会返回,并传出客户端的地址。
connect()
connect()
函数存在于函数库sys/socket.h
中,其声明如下:
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
connect()函数用于客户端,该函数的功能为向服务器发起连接请求。 connect()函数的参数与bind()函数中参数的形式⼀致,区别在于bind()中的参数为客户端进程地址,而connect的参数为服务器端地址。
connect()函数调用成功则返回0
,否则返回-1
,并设置errno
。
write()
函数定义:
ssize_t write (int fd, const void * buf, size_t count);
write()会把参数buf所指的内存写入count
个字节到参数放到所指的文件内。write成功返回,只是buf中的数据被复制到了kernel中的TCP发送缓冲区。至于数据什么时候被发往网络,什么时候被对方主机接收,什么时候被对方进程读取,系统调用层面不会给予任何保证和通知。
write在什么情况下会阻塞?当kernel的该socket的发送缓冲区已满时。
对于每个socket,拥有自己的send buffer
和receive buffer
。从Linux 2.6开始,两个缓冲区大小都由系统来自动调节(autotuning),但一般在default和max之间浮动。已经发送到网络的数据依然需要暂存在send buffer
中,只有收到对方的ack
后,kernel才从buffer中清除这一部分数据,为后续发送数据腾出空间。
接收端将收到的数据暂存在receive buffer
中,自动进行确认。但如果socket所在的进程不及时将数据从receive buffer
中取出,最终导致receive buffer
填满,由于TCP的滑动窗口和拥塞控制,接收端会阻止发送端向其发送数据。这些控制皆发生在TCP/IP栈中,对应用程序是透明的,应用程序继续发送数据,最终导致send buffer填满,write调用阻塞。
返回值:如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中。
send()
send()
函数函数存在于函数库sys/socket.h
中,其声明如下:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
该函数用于向处于连接状态的套接字中发送数据,
若调用成功都返回0
, 否则返回-1
,并设置errno
。
- 参数
sockfd
表示接收端的socket⽂件描述符; - 参数
buf
为指向要发送数据的缓冲区指针; - 参数
len
表示缓冲区buf中数据的长度; - 参数
flags
表示调用的执行方式(阻塞/非阻塞),当flags设置为0时,可使用之前学习的write()
函数替代send()
函数。
Linux系统中还提供了sendto()
函数和sendsg()
函数,这两个函数不但能发送数据给已连接的套接字,还可向未连接的套接字发送数据。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
sendto()
函数中的前4个参数与send()
函数的参数相同,之后的参数dest_addr
和addrlen
分别用于设置接收数据进程的地址和地址的长度;
sendmsg()
函数中的第⼆个参数msg
为struct msghdr
类型的结构体指针,该参数用于传入目标进程的地址、地址的长度等信息。
函数
send()
、sendto()
、sendmsg()
调用成功并不表示接收端套接字⼀定会接收到发送的数据。
recv()
recv()
函数存在于函数库sys/socket.h
中,其声明如下:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
该函数用于从已连接的套接字中接收信息,若调用成功则返回读到的字节数,否则返回-1
,并设置errno
。
此外,read()
函数、recvfrom()
函数和recvmsg()
函数也用于接收信息。
close()
close()函数用于释放系统分配给套接字的资源,该函数即文件操作 中常用于关闭文件的函数,存在于函数库unistd.h
中,其声明如下:
int close(int fd);
close()函数中的参数fd
为文件描述符,当其用于scoket编程中时,需传⼊socket⽂件描述符。该函数调用成功则返回0
,否则返回-1
,并设置 errno
。
10.4 socket通信流程
10.5 相关的知识
网络序字节
若将数据的高字节保存在内存的低地址,将数据的低字节保存在内存的高地址,这种存放方式称为大端模式;相反就是小端模式。
- 网络数据流:大端模式
- 计算机:大端/小端模式
Linux系统中提供了⼀些用于字节序转换的函数,这些函数存在于函 数库arpa/inet.h
中,它们的定义如下:
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
IP地址转换函数
标准的IPv4格式的地址,但这种格式是为了方便用户对其进行操作,若要使计算机能够识别,需先将其转换为二进制格式。
早期Linux系统中常使⽤以下函数来转换IP地址:
int inet_aton(const char *cp, struct in_addr *inp); in_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);
但以上函数只能处理IPv4的ip地址,且它们都是不可重⼊函数。
如今Linux编程中常⽤inet_pton()
和inet_ntop()
来转换IP地址,这两个函数不但能转换IPv4格式的地址in_addr
,还能转换IPv6格式的地址in_addr6
,它们存在于函数库arpa/inet.h
中,函数定义如下:
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
sockaddr数据结构
IPv4和IPv6的地址格式定义在netinet/in.h
中:
- IPv4地址用结构体
sockaddr_in
表示,该结构体中包含16位的端口号和32位的IP地址; - IPv6地址用结构体
sockaddr_in6
表示,该结构体中包含16位的端口号、128位的IP地址和⼀些控制字段; - Unix Domain Socket的地址格式定义在
sys/un.h
中,用结构体sock_addr_un
表示
bind()
、accept()
、connect()
等函数的参数应该设计成void *
类型,以便接收各种类型的指针,但是sock API
的实现早于ANSI C
标准化,那时还没有void *
类型,因此这些函数的参数都用struct sockaddr *
类型表示,在传递参数之前要进行强制类型转换,例如:
struct sockaddr_in servaddr;
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
10.6 socket本地进程通信
socket原本是为网络通讯设计的,但后来在socket框架的基础上发展出了⼀ 种IPC(进程通信)机制,即UNIX Domain Socket,专门⽤来实现使⽤socket实现的本地进程通信。
本地通信的流程与使⽤的接口与基于TCP协议的网络通信模型相同,其大致流程如下:
1.调⽤socket()
函数通信双方进程创建各⾃的socket⽂件;
2.定义并初始化服务器端进程的地址,并使用bind()
函数将其与服务器端进程绑定;
3.调用listen()
函数监听客户端进程请求;
4.客户端调用connect()
函数,根据已明确的客户端进程地址,向服务器发送请求;
5.服务器端调⽤accept()
函数,处理客户端进程的请求,若客户端与服务器端进程成功建⽴连接,则双方进程可开始通信;
6.通信双方以数据流的形式通过已创建的连接互相发送和接收数据,进行通信;
7.待通信结束后,通信双方各自调用close()
函数关闭连接。
与socket网络通信不同的是,在本地通信中用到的套接字的结构体类型为socket sockaddr_un
。
实例:编写C/S结构的程序,分别创建服务器和客户端,客户端发送字符串,服务器端处理并返回
题目
编写C/S结构的程序,分别创建服务器和客户端。
客户端的功能是从终端获取⼀个字符串发送给服务器,然后接收服务器返回的字符串并打印;服务器的功能是从客户端发来的字符,将每个字符转换为⼤写再返回给客户端。
要求使⽤TCP协议实现。
参考文档
socket编程在windows和linux下的区别
windows下linux下socket编程区别
tcpclient_linux
代码
/*
* @Description:
* @Author: dive668
* @Date: 2021-07-28 22:39:28
* @LastEditTime: 2021-07-29 10:09:54
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argv,char *argv[])
{
struct sockaddr_in servaddr; //定义通信进程的地址
char buf[MAXLINE]; //定义缓冲区,用于存放数据
int sockfd,n; //创建两个socket的文件描述符
char *str; //定义用户字符输入
if(argc!=2) //如果参数数量不为2,即没有带一个参数运行此程序
{
fputs("usage:./client message\n",stderr);
exit(1);
}
str=argv[1]; //第一个字符参数赋值给str
sockfd=socket(AF_INET,SOCK_STREAM,0);//地址类型为AF_INET,连接为TCP,采用默认协议
//用于打开网络通讯端口,调用成功则返回一个文件描述符,调用失败则返回-1
bzero(&servaddr,sizeof(servaddr)); //结构体清零
servaddr.sin_family=AF_INET; //设置地址类型为AF_INET
inet_pton(AF_INET,"127.0.0.1",&servaddr.sin_addr);
servaddr.sin_port=htons(SERV_PORT); //设置端口号为宏定义的端口SERV_PORT
connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
//用于客服端,向服务器端发送请求,类似于bind,调用成功则返回0,否则返回-1
send(sockfd,str,strlen(str),0);
//向处于连接状态的套接字中发送数据
//connfd为接收端的socket⽂件描述符
//buf为要发送的缓冲区中的数据长度
//flag表示调用的执行方式,设为0则可用write代替send函数
n=recv(sockfd,buf,MAXLINE,0);
//从已连接的套接字中接收信息,若调用成功则返回读到的字节数
printf("Response from setver:\n");
write(STDOUT_FILENO,buf,n);
//write把buf指定的内存写入n个字节,放入到指定的socket文件内
close(sockfd);
return 0;
}
tcpserver_linux
代码
/*
* @Description:
* @Author: dive668
* @Date: 2021-07-29 09:22:52
* @LastEditTime: 2021-07-29 09:49:25
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
'''
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
'''
#define MAXLINE 80
#define SERV_PROT 6666
int main(void)
{
struct sockaddr_in servaddr,cliaddr; //定义通信进程的地址
socklen_t cliaddr_len; //定义addr的长度
int listenfd,connfd; //创建两个socket的文件描述符
char buf[MAXLINE];
char str[INET_ADDRSERLEN];
int i,n;
listenfd=socket(AF_INET,SOCK_STEAM,0);
//用于打开网络通讯端口,调用成功则返回一个文件描述符,调用失败则返回-1
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(SERV_PROT); //设置端口号为宏定义的端口6666
bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)); //将socket文件与进程绑定
listen(listenfd,20); //设置服务器可建立连接的数量为20
printf("Accepting connections...\n");
while(1)
{
cliaddr_len=sizeof(cliaddr);
connfd=accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len); //处理客户端连接请求
n=recv(connfd,buf,MAXLINE,0); //从已连接的套接字中接收信息,若调用成功则返回读到的字节数
printf("received from %s at PORT %d\n",inet_stop(AF_INET,&cliaddr.sin_addr,str,sizeof(stderr),ntohs(cliaddr.sin_port));
for(i=0;i<n;++i)
buf[i]=toupper(buf[i]); //处理缓冲区的数据
send(connfd,buf,n,0);
//向处于连接状态的套接字中发送数据
//connfd为接收端的socket⽂件描述符
//buf为要发送的缓冲区中的数据长度
//flag表示调用的执行方式,设为0则可用write代替send函数
close(connfd);//关闭连接
}
return 0;
}