--系统编程-网络-tcp客户端服务器编程模型、socket、htons、inet_ntop等各API详解、使用telnet测试基本服务器功能
PART1 基础知识
1. 字节序
网络字节序是大端字节序(低地址存放更高位的字节), 所以,对于字节序为小端的机器需要收发网络数据的场景,要对这些数据进行字节序转换。
字节序转换函数,常用的有四个:
很好记,n表示network, h表示host, l表示long, s表示short。
举例, htons 表示将主机的二字节数据转为网络字节序。
PART2 TCP客户端、服务器 的编程模型 总体概述 以及涉及到的API详解
1. socket套接字的背景介绍
注意,这里相关描述的组成有五个部分,这就是广为人知的 socket 五元组 =》{协议、本地地址、本地端口、远程地址、远程端口}
socket在内核中的位置如下图
2. socket系统调用正式登场
对于socket的第二个参数type,大家一般知道的和最常用的只是对应TCP和UDP的两种,实际上一共有4个可选参数哦!
3. 网络通信,肯定需要IP地址,本步骤就是填充好客户端或服务器编程所需的地址信息参数(两种方式)
方式1 通用地址结构, 使用不方便 ,介绍如下
方式2 因特网地址结构,使用更加方便,介绍如下
填充ipv4地址 -- 使用示例 (指定服务器所使用的网卡IP为192.168.2.1) :
在填充struct sockaddr_in类型的结构体变量sin的部分成员时,我们使用到了辅助函数inet_pton。
功能: 将点分十进制的ip地址转化为用于网络传输的数值格式
返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1 .
功能: 将数值格式转化为点分十进制的ip地址格式
返回值:若成功则为指向结构的指针,若出错则为NULL
好记忆, inet_pton中的p表示pointer,即点分十进制的字符串,而n表示network,即网络字节序的数据。
所以inet_pton表示将点分十进制的字符串转为网络字节序数据, inet_ntop表示将网络字节序转为点分十进制数据。
详解:
(1)这两个函数的af,即family参数,既可以是AF_INET(ipv4), 也可以是AF_INET6(ipv6).
如果,以不被支持的地址族作为family参数,这两个函数都会返回一个错误,并将errno置为EAFNOSUPPORT.
(2)inet_pton函数尝试转换由src指针所指向的字符串,并通过dst指针存放二进制结果,
成功则返回值为1.失败则返回值为0,例如所指定的family不是有效的表达式格式时.
(3)inet_ntop函数进行相反的转换=》从数值格式(src)转换到表达式(dst)。
inet_ntop函数的dst参数不可以是一个空指针,调用者必须为目标存储单元分配内存并指定其大小,调用成功时,这个指针就是该函数的返回值。
size参数是目标存储单元的大小,以免该函数溢出其调用者的缓冲区。如果size太小,不足以容纳表达式结果,那么返回一个空指针,并置为errno为ENOSPC。
PS:在上图示例代码内,指定了服务器所使用的网卡IP为192.168.2.1,我们也可以监听服务器所在主机的所有网卡,即使用INADDR_ANY,如下图所示
4. 对于服务器编程,接着需要调用bind来绑定上一步骤内准备好的地址信息
返回:成功则返回 0, 出错返回 -1 。
5. 对于服务器编程,下一步就是让服务器端监听客户端连接,涉及listen系统调用
listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。
listen()成功返回0,失败返回-1。
6. 对于服务器编程,接着就是让服务器调用accept()接受客户端的连接
如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。即:若没有客户端连接,调用此函数会阻塞。
addr是一个传出参数,accept()返回时传出客户端的地址和端口号。如果给addr参数传NULL,表示不关心客户端的地址。
addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的缓冲区的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区).
accept 使用说明1:
PS: 这里同时获取了连接上来的客户端的信息,使用下图方式可以解析出该信息
accept使用说明2:
我们的服务器程序结构如下
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE);
......
close(connfd);
}
整个是一个while死循环,每次循环处理一个新客户端的连接。
由于cliaddr_len是一个传入传出参数(value-result argument), 传入的是调用者提供的缓冲区的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区).所以,每次调用accept()之前应该重新赋初值。
accept()的参数sockfd一直都是服务器本地负责监听的文件描述符,而accept()的返回值是针对新连接上来的客户端的文件描述符connfd,
之后与客户端之间就通过这个connfd通讯,最后关闭connfd断开与该客户端的连接。
7. 对于服务器编程,下一步就可以调用read、write系统调用进行数据传输了
7.1 write函数
write函数将buf中的nbytes字节内容写入到文件描述符中,成功返回写的字节数,失败返回-1.并设置errno变量。在网络程序中,当我们向套接字文件描述舒服写数据时有两种可能:
7.1.1、网络络编程中写函数是不负责将全部数据写完之后再返回的,说不定中途就返回了!
write的返回值大于0,表示写了部分数据或者是全部的数据。
一般用一个while循环不断的写入数据,循环过程中的buf参数和nbytes参数是需要我们来把控的。
下面展示一个发送大数据量时的核心代码,准确可靠的
7.1.2、write函数返回值小于0,表示出错了,可查看errno,需要根据错误类型进行相应的处理。
如果错误码是EINTR,表示在写的时候出现了中断错误,如果错误码是EPIPE,表示网络连接出现了问题。
7.2 read函数
read函数是负责从fd中读取内容,当读取成功时,read返回实际读取到的字节数,如果返回值是0,表示已经读取到文件的结束了,小于0表示是读取错误。
如果错误是EINTR,表示在写的时候出现了中断错误,如果错误码是EPIPE,表示网络连接出现了问题。
7.3 recv 和 send 函数
前面的三个参数和read、write函数是一样的。
第四个参数可以是0或者是以下组合
MSG_DONTROUTE:不查找表
send函数使用的标志,这个标志告诉IP,目的主机在本地网络上,没有必要查找表,这个标志一般用在网络诊断和路由程序里面。
MSG_OOB:接受或者发生带外数据
表示可以接收和发送带外数据。
MSG_PEEK:查看数据,并不从系统缓冲区移走数据
recv函数使用的标志,表示只是从系统缓冲区中读取内容,而不清楚系统缓冲区的内容。这样在下次读取的时候,依然是一样的内容,一般在有多个进程读写数据的时候使用这个标志。
MSG_WAITALL:等待所有数据
recv函数的使用标志,表示等到所有的信息到达时才返回,使用这个标志的时候,recv返回一直阻塞,直到指定的条件满足时,或者是发生了错误。
8. close 关闭文件描述符
成功则返回0,错误返回 -1,
错误码errno为EBADF,表示fd不是一个有效描述符; 错误码errno为EINTR,表示close函数被信号中断;而EIO则表示一个IO错误。
9. 客户端建立连接
成功则返回0, 出错返回 -1, 并设置errno。
客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于服务器bind的参数是服务器自己的地址,而客户端connect的参数是服务器的地址。
PART3 基本服务器端程序编写、使用telnet客户端,针对自己编写好的服务器,进行功能测试
如果遇到上述问题,解决很简单,按下图开启telnet client功能即可。
实验代码:
server.h
#ifndef __SERVER_H__ #define __SERVER_H__ #include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */ #define SERV_PORT 5001
#define SERV_IPADDR "192.168.1.21"
#define QUIT_STR "quit" #endif
server.c
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h> #include <stdint.h> #include <string.h>
#include "server.h"
#include <assert.h> #include <sys/types.h>
#include <unistd.h>
#include <signal.h> void my_itoa(long i, char *string)
{
int power = 0, j = 0; j = i;
for (power = 1; j>10; j /= 10)
power *= 10; for (; power>0; power /= 10)
{
*string++ = '0' + i / power;
i %= power;
}
*string = '\0';
printf("%s\n", string);
} int server_local_fd, new_client_fd; void sig_deal(int signum){ close(new_client_fd);
close(server_local_fd);
exit(1);
} int main(void)
{
struct sockaddr_in sin; signal(SIGINT, sig_deal); printf("pid = %d \n", getpid()); /*1.创建IPV4的TCP套接字 */
server_local_fd = socket(AF_INET, SOCK_STREAM, 0);
if(server_local_fd < 0) {
perror("socket error!");
exit(1);
} /* 2.绑定在服务器的IP地址和端口号上*/
/* 2.1 填充struct sockaddr_in结构体*/
bzero(&sin, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(SERV_PORT); #if 1
// 方式一
sin.sin_addr.s_addr = inet_addr(SERV_IPADDR);
#endif #if 0
// 方式二:
sin.sin_addr.s_addr = INADDR_ANY;
#endif #if 0
// 方式三: inet_pton函数来填充此sin.sin_addr.s_addr成员
if(inet_pton(AF_INET, "192.168.1.21", &sin.sin_addr.s_addr) >0 ){
printf("s_addr=%s \n", inet_ntop(AF_INET, &sin.sin_addr.s_addr, buf, sizeof(buf)));
printf("buf = %s \n", buf); // 这两条打印语句结果是一样的, inet_ntop调用成功的返回值就是buf。
}
#endif /* 2.2 绑定*/
if(bind(server_local_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
perror("bind");
exit(1);
} /*3.listen */
listen(server_local_fd, 5); printf("client listen 5. \n"); char sned_buf[] = "hello, i am server \n"; struct sockaddr_in clientaddr;
socklen_t clientaddrlen; while(1) { /*4. accept阻塞等待客户端连接请求 */
#if 0
/*****不关心连接上来的客户端的信息*****/ if( (new_client_fd = accept(server_local_fd, NULL, NULL)) < 0) { }else{
/*5.和客户端进行信息的交互(读、写) */
ssize_t write_done = write(new_client_fd, sned_buf, sizeof(sned_buf));
printf("write %ld bytes done \n", write_done); }
#else
/****获取连接上来的客户端的信息******/ memset(&clientaddr, 0, sizeof(clientaddr));
memset(&clientaddrlen, 0, sizeof(clientaddrlen)); clientaddrlen = sizeof(clientaddr);
/***
* 由于cliaddr_len是一个传入传出参数(value-result argument),
* 传入的是调用者提供的缓冲区的长度以避免缓冲区溢出问题,
* 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区).
* 所以,每次调用accept()之前应该重新赋初值。
* ******/
if( (new_client_fd = accept(server_local_fd, (struct sockaddr*)&clientaddr, &clientaddrlen)) < 0) {
perror("accept");
exit(1);
} printf("client connected! print the client info .... \n");
int port = ntohs(clientaddr.sin_port);
char ip[16] = {0};
inet_ntop(AF_INET, &(clientaddr.sin_addr.s_addr), ip, sizeof(ip));
printf("client: ip=%s, port=%d \n", ip, port); #endif } close(new_client_fd);
close(server_local_fd); return 0;
}
在windows下确保能够ping通我们的ubuntu,然后开启两个telnet去连接我们的服务器程序,结果如下
ubuntu内编译好代码,运行结果如下
后记,本文已过长,待补充下图俩知识点:
.
--系统编程-网络-tcp客户端服务器编程模型、socket、htons、inet_ntop等各API详解、使用telnet测试基本服务器功能的更多相关文章
- 系统编程-网络-tcp客户端服务器编程模型(续)、连接断开、获取连接状态场景
相关博文: 系统编程-网络-tcp客户端服务器编程模型.socket.htons.inet_ntop等各API详解.使用telnet测试基本服务器功能 接着该上篇博文,咱们继续,首先,为了内容的完整性 ...
- 网络编程socket基本API详解(转)
网络编程socket基本API详解 socket socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信. socket ...
- 【Unity编程】Unity中关于四元数的API详解
本文为博主原创文章,欢迎转载,请保留出处:http://blog.csdn.net/andrewfan Unity中关于四元数的API详解 Quaternion类 Quaternion(四元数)用于计 ...
- 理论经典:TCP协议的3次握手与4次挥手过程详解
1.前言 尽管TCP和UDP都使用相同的网络层(IP),TCP却向应用层提供与UDP完全不同的服务.TCP提供一种面向连接的.可靠的字节流服务. 面向连接意味着两个使用TCP的应用(通常是一个客户和一 ...
- TCP协议的3次握手与4次挥手过程详解
1.前言 尽管TCP和UDP都使用相同的网络层(IP),TCP却向应用层提供与UDP完全不同的服务.TCP提供一种面向连接的.可靠的字节流服务. 面向连接意味着两个使用TCP的应用(通常是一个客户和一 ...
- 【网络编程1】网络编程基础-TCP、UDP编程
网络基础知识 网络模型知识 OSI七层模型:(Open Systems Interconnection Reference Model)开放式通信系统互联参考模型,是国际标准化组织(ISO)提出的一个 ...
- Linux C 网络编程——3. TCP套接口编程
1. 基本流程 2. socket() int socket(int domain, int type, int protocol); socket()打开一个网络通讯端口,如果成功的话,就像open ...
- python编程系列---tcp客户端的简单实现
实现流程如下: """ TCP客户端实现流程1. 创建一个tcp 客户端对象2. 与服务端建立连接3. 通过tcp socket 收发数据4. 关闭连接 关闭tcp &q ...
- 【转】网络编程socket基本API详解
转自:http://www.cnblogs.com/luxiaoxun/archive/2012/10/16/2725760.html socket socket是在应用层和传输层之间的一个抽象层,它 ...
随机推荐
- c++ 动态解析PE导出表
测试环境是x86 main #include <iostream> #include <Windows.h> #include <TlHelp32.h> #incl ...
- java数据类型(进阶篇)
public class note03 { public static void main(String[] args) { //数据类型拓展 //1.整数拓展 //进制: 二进制0b 十进制 八进制 ...
- Python爬虫_豆瓣电视剧
1 import requests 2 import json 3 import csv 4 5 6 class DoubantvSpider: 7 def __init__(self): 8 # s ...
- 如何用python自动编写《赤壁赋》word文档
目录 前言 安装-python-docx 一.自动编写<赤壁赋> 准备数据 新建文档 添加标题 添加作者 添加朝代 添加图片 添加段落 保存word文档 二.自动提取<赤壁赋> ...
- Lambda 表达式简介
0.预备知识 函数式接口:只包含一个抽象方法的接口. 内部类:静态.成员内部类 局部内部类 匿名内部类 1.代码 1 /** 2 * 函数式编程: 3 * lambda表达式前提: 4 * 必须是函数 ...
- 如何用Eggjs从零开始开发一个项目(3)
上一篇中我们编写了用户注册登录.登录的代码,学习了如何进行用户的认证(JWT),如何安全地存储用的密码(hash).这一篇我们有以下2个任务: 获取token中的数据: 通过model来同步数据库. ...
- XAPKInstaller - XAPK游戏包安装器
XAPKInstaller 一个用于安装XAPK游戏包的安装器. 程序需要读写存储与获取已安装应用权限才可正常运行. 长按条目可显示文件的详细信息. SDK小于24(Android N)的设备会显示应 ...
- 通过golang小案例,了解golang程序常见机制
目录 代码理解及纠错 1.defer和panic执行先后顺序 2.for循环元素副本问题 3.slice追加元素问题 4.返回值命名问题 5.用new初始化内置类型问题 6.切片append另外一个切 ...
- AJAX基本操作
XMLHttpRequest对象: XMLHttpRequest 是 AJAX 的基础.所有现代浏览器均支持 XMLHttpRequest 对象(IE5 和 IE6 使用 ActiveXObject) ...
- HDOJ-6645(简单题+贪心+树)
Stay Real HDOJ-6645 由小根堆的性质可以知道,当前最大的值就在叶节点上面,所以只需要排序后依次取就可以了. #include<iostream> #include< ...