目录
网络知识介绍
Linux网络编程--初等网络函数介绍(TCP)
server.c(例程)
client.c(例程)
网络编程的一个最大的特点就是网络程序是由两个部分组成--客户端和服务器端。
客户端:在网络程序中,如果一个程序主动和外面的程序通信,那么我们把这个程序称为客户端程序。
服务端: 和客户端相对应的程序即为服务端程序。被动的等待外面的程序来和自己通讯的程序称为服务端程序。
实际生活中有些程序是互为服务和客户端。在这种情况项目, 一个程序既为客户端也是服务端。
Linux系统通过提供套接字(socket)来进行网络编程的.网络程序通过调用socket和其它几个函数,返回一个通讯的文件描述符,我们可以将这个描述符看成普通的文件的描述符来操作,这就是linux的设备无关性的好处. 我们可以通过向描述符读写操作实现网络之间的数据交流.
服务器端:
(1)socket:相当于文件操作的open函数,用于创建一个类似于文件句柄的句柄fd
int socket(int domain, int type, int protocol)
//domain:说明我们网络程序所在的主机采用的通讯协族(AF_UNIX和AF_INET等). AF_UNIX只能够用于单一的Unix 系统进程间通信,而AF_INET是针对Internet的,因而可以允许在远程
//type:我们网络程序所采用的通讯协议(SOCK_STREAM,SOCK_DGRAM等),SOCK_STREAM表明我们用的是TCP 协议,这样会提供按顺序的,可靠,双向,面向连接的比特流.SOCK_DGRAM 表明我们用的是UDP协议,这样只会提供定长的,不可靠,无连接的通信.
//protocol:由于我们指定了type,所以这个地方我们一般只要用0来代替就可以了 socket为网络通讯做基本的准备.
(2)bind:一般打开文件也需要如路径等参数,进而将fd文件句柄与目标路径绑定,这里的bind也是类似的作用,但绑定的是主机的通讯簇类,端口,主机IP等信息。
int bind(int sockfd, struct sockaddr *my_addr, int addrlen)
//sockfd:是由socket调用返回的文件描述符.
//addrlen:是sockaddr结构的长度.
//my_addr:是一个指向sockaddr的指针.
struct sockaddr
{
unisgned short as_family;
char sa_data[14];
};
//不过由于系统的兼容性,我们一般不用这个头文件,而使用另外一个结构(struct sockaddr_in) 来代替.在中有sockaddr_in的定义
struct sockaddr_in
{
unsigned short sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
/*
我们主要使用Internet所以
sin_family一般为AF_INET,
sin_addr设置为INADDR_ANY表示可以和任何的主机通信,
sin_port是我们要监听的端口号,
sin_zero[8]是用来填充的,
bind将本地的端口同socket返回的文件描述符捆绑在一起.成功是返回0,失败的情况和socket一样
*/
(3)listen:用于监听服务器fd的信息
int listen(int sockfd,int backlog)
//sockfd:是bind后的文件描述符.
//backlog:设置请求排队的最大长度.当有多个客户端程序和服务端相连时, 使用这个表示可以接受的排队长度.listen函数将bind的文件描述符变为监听套接字.返回的情况和bind一样.
(4)accept:服务器端与客户端建立连接的函数,用于被动地等待客户端的连接请求
int accept(int sockfd, struct sockaddr *addr,int *addrlen)
//sockfd:是listen后的文件描述符.
// addr,addrlen是用来给客户端的程序填写的,服务器端只要传递指针就可以了. bind,listen和accept是服务器端用的函数,accept调用时,服务器端的程序会一直阻塞到有一个客户程序发出了连接.accept成功时返回最后的服务器端的文件描述符,这个时候服务器端可以向该描述符写信息了. 失败时返回-1
客户器端:
(5)connect:用于向服务端请求连接
int connect(int sockfd, struct sockaddr * serv_addr,int addrlen)
//sockfd:socket返回的文件描述符.
//serv_addr:储存了服务器端的连接信息.其中sin_add是服务端的地址
//addrlen:serv_addr的长度
//connect函数是客户端用来同服务端连接的.成功时返回0,sockfd是同服务端通讯的文件描述符 失败时返回-1.
这里以韦东山的代码为例子,大致讲解一下下面这份代码各个部分的作用
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
#include
#include
/* socket
* bind
* listen
* accept
* send/recv
*/
#define SERVER_PORT 8888
#define BACKLOG 10
int main(int argc, char **argv)
{
int iSocketServer;
int iSocketClient;
struct sockaddr_in tSocketServerAddr;
struct sockaddr_in tSocketClientAddr;
int iRet;
int iAddrLen;
int iRecvLen;
unsigned char ucRecvBuf[1000];
int iClientNum = -1;
signal(SIGCHLD,SIG_IGN);
iSocketServer = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == iSocketServer)
{
printf("socket error!\n");
return -1;
}
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT); /* host to net, short */
tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;
memset(tSocketServerAddr.sin_zero, 0, 8);
iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (-1 == iRet)
{
printf("bind error!\n");
return -1;
}
iRet = listen(iSocketServer, BACKLOG);
if (-1 == iRet)
{
printf("listen error!\n");
return -1;
}
while (1)
{
iAddrLen = sizeof(struct sockaddr);
iSocketClient = accept(iSocketServer, (struct sockaddr *)&tSocketClientAddr, &iAddrLen);
if (-1 != iSocketClient)
{
iClientNum++;
printf("Get connect from client %d : %s\n", iClientNum, inet_ntoa(tSocketClientAddr.sin_addr));
if (!fork())
{
while (1)
{
iRecvLen = recv(iSocketClient, ucRecvBuf, 999, 0);
if (iRecvLen <= 0)
{
close(iSocketClient);
return -1;
}
else
{
ucRecvBuf[iRecvLen] = '\0';
printf("Get Msg From Client %d: %s\n", iClientNum, ucRecvBuf);
}
}
}
}
}
close(iSocketServer);
return 0;
}
第一部分:socket(服务端套接字创建)
iSocketServer = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == iSocketServer)
{
printf("socket error!\n");
return -1;
}
/*
这段代码创建了一个用于 TCP 通信的服务器端套接字。其中,socket 函数用于创建一个新的套接字,它接受三个参数:
AF_INET:表示使用 IPv4 协议;
SOCK_STREAM:表示使用面向连接的流式套接字;(面向连接即TCP协议)
0:表示使用默认协议,通常为 TCP 协议。
如果 socket 函数返回值为 -1,说明创建套接字失败,此时会输出一条错误信息 "socket error!\n",并返回 -1。
如果成功创建套接字,会将其赋值给变量 iSocketServer,之后可以使用该套接字进行服务器端的网络通信。
*/
第二部分:bind(服务端套接字与服务端IP地址,端口号等信息绑定)
//前面的四句都是对结构体内容的填充
tSocketServerAddr.sin_family = AF_INET; //指定了地址族AF_INET(IPv4)其他参数:AF_INET6(IPv6)
tSocketServerAddr.sin_port = htons(SERVER_PORT); //指定了端口号,一般使用 htons 函数将端口号转换为网络字节序
tSocketServerAddr.sin_addr.s_addr = INADDR_ANY; //INADDR_ANY 表示绑定到本地所有可用的 IP 地址。如ifconfig命令打印的eth33与eth36网卡信息中IP地址不同,INADDR_ANY将使得服务器端可以监听到全部网卡的IP地址。
memset(tSocketServerAddr.sin_zero, 0, 8); //sin_zero 用于将结构体补齐到与 struct sockaddr 结构体相同的长度。
iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (-1 == iRet)
{
printf("bind error!\n");
return -1;
}
/*
bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
第一个参数 sockfd 是服务器端套接字的文件描述符,它是由 socket 函数创建得到的。
第二个参数 addr 是一个 sockaddr 类型的指针,它指向要绑定的 IP 地址和端口号。
第三个参数 addrlen 是 addr 的大小,即要绑定的地址结构体的大小。
在该代码段中,第一个参数使用服务器端套接字的文件描述符 iSocketServer。第二个参数使用了结构体变量 tSocketServerAddr,该结构体变量包含了服务器端 IP 地址和端口号的信息
*/
第三部分:listen(开始监听服务器端套接字 iSocketServer,等待客户端的连接请求)
iRet = listen(iSocketServer, BACKLOG);
if (-1 == iRet)
{
printf("listen error!\n");
return -1;
}
/*
listen(iSocketServer, BACKLOG);
第一个参数 sockfd 是服务器端套接字的文件描述符,它是由 socket 函数创建得到的。
第二个参数 backlog 指定了连接队列的最大长度,也就是可以同时处理的等待连接请求的客户端的数量。
*/
第四部分:accept与send(之所以把这两个放在一起说,也是因为好像没办法细分了,)
/*
这段代码使用 accept 函数来接收客户端的连接请求,并启动子进程来处理客户端发来的消息。具体而言,while 循环会一直运行,等待客户端的连接请求。当一个客户端连接成功后,accept 函数会返回一个新的套接字 iSocketClient,该套接字用于和该客户端进行通信。
*/
while (1)
{
iAddrLen = sizeof(struct sockaddr);
//接受客户端连接请求,并返回一个客户端套接字iSocketClient
iSocketClient = accept(iSocketServer, (struct sockaddr *)&tSocketClientAddr, &iAddrLen);
//如果接收到连接请求,则建立连接
if (-1 != iSocketClient)
{
//客户端计数变量加1,并打印连接的客户端的IP地址信息
iClientNum++;
printf("Get connect from client %d : %s\n", iClientNum, inet_ntoa(tSocketClientAddr.sin_addr));
//开启一个子进程,在if分支为子进程,else分支为主进程
if (!fork())
{
//在子进程中,程序会一直运行一个 while 循环,等待客户端发来的消息。如果接收到的消息长度小于等于 0,则表示客户端已经关闭连接,此时程序会关闭 iSocketClient 套接字,然后返回 -1,退出子进程。否则,程序会输出接收到的消息,然后继续等待客户端的下一条消息。
while (1)
{
iRecvLen = recv(iSocketClient, ucRecvBuf, 999, 0);
if (iRecvLen <= 0)
{
close(iSocketClient);
return -1;
}
else
{
ucRecvBuf[iRecvLen] = '\0';
printf("Get Msg From Client %d: %s\n", iClientNum, ucRecvBuf);
}
}
}
}
}
//关闭服务器端套接字 iSocketServer
close(iSocketServer);
附:僵尸进程解决(听课听得不是很认真,忘记咋形成了,后面有空再补充)
signal(SIGCHLD,SIG_IGN);
/*
这段代码是用来忽略 SIGCHLD 信号的,具体而言,它将 SIGCHLD 信号的处理方式设置为忽略。SIGCHLD 信号是在一个子进程终止或者停止时,由内核向其父进程发送的信号。如果父进程不对该信号进行处理,那么子进程就会变成僵尸进程,占用系统资源。因此,通常情况下,在父进程中需要对 SIGCHLD 信号进行处理,以避免产生僵尸进程。
有些情况下,我们希望让内核自动回收子进程,而不需要父进程对 SIGCHLD 信号进行处理。这时,我们可以使用 signal 函数将 SIGCHLD 信号的处理方式设置为 SIG_IGN(即忽略该信号),这样内核就会在子进程终止时自动回收它们,不会产生僵尸进程。
*/
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
#include
/* socket
* connect
* send/recv
*/
#define SERVER_PORT 8888
int main(int argc, char **argv)
{
int iSocketClient;
struct sockaddr_in tSocketServerAddr;
int iRet;
unsigned char ucSendBuf[1000];
int iSendLen;
if (argc != 2)
{
printf("Usage:\n");
printf("%s \n", argv[0]);
return -1;
}
iSocketClient = socket(AF_INET, SOCK_STREAM, 0);
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT); /* host to net, short */
//tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;
if (0 == inet_aton(argv[1], &tSocketServerAddr.sin_addr))
{
printf("invalid server_ip\n");
return -1;
}
memset(tSocketServerAddr.sin_zero, 0, 8);
iRet = connect(iSocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (-1 == iRet)
{
printf("connect error!\n");
return -1;
}
while (1)
{
if (fgets(ucSendBuf, 999, stdin))
{
iSendLen = send(iSocketClient, ucSendBuf, strlen(ucSendBuf), 0);
if (iSendLen <= 0)
{
close(iSocketClient);
return -1;
}
}
}
return 0;
}
第一部分:socket(与服务端一般无二,都是创建相同类型的数据结构而已)
iSocketClient = socket(AF_INET, SOCK_STREAM, 0);
/*
这段代码创建了一个用于 TCP 通信的服务器端套接字。其中,socket 函数用于创建一个新的套接字,它接受三个参数:
AF_INET:表示使用 IPv4 协议;
SOCK_STREAM:表示使用面向连接的流式套接字;(面向连接即TCP协议)
0:表示使用默认协议,通常为 TCP 协议。
*/
第二部分:conenect
tSocketServerAddr.sin_family = AF_INET; //使用 IPv4 地址族
tSocketServerAddr.sin_port = htons(SERVER_PORT); //将 sin_port 成员设置为服务器监听的端口号 SERVER_PORT,并使用 htons() 函数将主机字节序转换为网络字节序。
//代码使用 inet_aton() 函数将命令行参数 argv[1](表示服务器的 IP 地址)转换为网络字节序的二进制 IP 地址,并将其存储到 tSocketServerAddr.sin_addr 成员中。如果转换失败,函数会打印错误信息并返回 -1。
if (0 == inet_aton(argv[1], &tSocketServerAddr.sin_addr))
{
printf("invalid server_ip\n");
return -1;
}
memset(tSocketServerAddr.sin_zero, 0, 8);
//它使用 connect() 函数,将创建的客户端套接字 iSocketClient 连接到指定的服务器地址 tSocketServerAddr,并传递一个指向该地址结构的指针。sizeof(struct sockaddr) 是地址结构的大小,这里需要将其传递给 connect() 函数,以指明要连接的地址结构的大小。
iRet = connect(iSocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (-1 == iRet)
{
printf("connect error!\n");
return -1;
}
第三部分:send/recv(每次从标准输入读取用户输入的字符串,然后使用 send()
函数将字符串发送到服务器)
while (1)
{
if (fgets(ucSendBuf, 999, stdin))
{
//send() 函数将 ucSendBuf 中的字符串发送到连接的服务器,参数 iSendLen 返回发送的字节数
iSendLen = send(iSocketClient, ucSendBuf, strlen(ucSendBuf), 0);
if (iSendLen <= 0)
{
//如果 iSendLen 小于或等于 0,则意味着发送失败或连接已关闭。在这种情况下,应该关闭客户端套接字并退出程序。
close(iSocketClient);
return -1;
}
}
}
综上,TCP通信编程的逐帧解析就到此为止,到时候UDP也会更新,但是可能晚点了,因为用的不是特别多