根据教师ppt,介绍网络编程(Network Programming)的基本概念,展示一些简单的程序和一些基本的概念(进程,系统调用,文件标识符,信号)。
目录
一.Network Programming
二.使用Linux进行C语言程序开发的基本知识
三.网络编程相关的基本概念
3.1socket套接字
3.2process进程
3.2.1 进程,母进程,PID
3.2.2 相关系统调用
3.2.3 代码示例,重点解释fork和execve这两个函数
3.3file descriptor文件描述符
3.3.1文件描述符和文件读写方式
3.3.2 文件访问权限
3.3.3 文件权限常量,文件打开方式常量,光标位置常量
3.3.4 与文件读写相关的系统调用
3.3.5 代码示例
3.4system call系统调用
3.5signal信号
一.Network Programming
教师给出的定义:
可见:1.首先,网络编程的目的是为了写出能够利用network进行远程通信的程序(program)(程序一旦运行就可以被认为是进程,而进程是通信的对象或说端点)。
2.其次,这里由于是在讲编程,所以用了术语program。准确的说法应该是process,因为网络通信的端点是进程。进程的概念在本篇文章的下半部分会讲。
教师进一步限定了我们这门课程所说的NP的范围:我们基于Linux OS、C语言、Ethernet和TCP/IP协议进行网络编程,并且为了隐藏运输层及其以下各层的实现细节,我们使用socket(套接字)编程,这样我们就可以如上图所示利用各种系统调用(system call)来使用底层的协议(TCP/UDP/IP等等)。可见我们专注于应用层协议的实现(写出的程序是应用层的)。
二.使用Linux进行C语言程序开发的基本知识
编译源文件,并指定产生的可执行文件的名字:
gcc helloworld.c -o helloworld.out
man查询手册
man 1 +命令 这里的1表示为查询的是Linux命令
man 2 xxx 这里的2表示为查询的是linux api
man 3 xxx 这里的3表示为查询的是C库函数
三.网络编程相关的基本概念
3.1socket套接字
具体的解释在我的这篇文章里面:(13条消息) C/Linux网络编程III--套接字与两个重要的结构体_竹某的博客-CSDN博客
目前就理解为“进程模型”,等于“主机IP地址+进程端口号”,标识了通信的一个端点。
一个socket pair标识了一个通信过程。
3.2process进程
3.2.1 进程,母进程,PID
教师给出的定义:
理解process的定义,就需要分清楚process和program。两者的关系就像Java中object和class的关系,前者是系统分配内容的基本单位,而后者只是静态的代码而已。所以,一个程序自然可以对应很多进程;而一个进程可以调用很多程序,使之变为进程(其子进程,对应一个母进程)。
每一个进程都有其唯一的标识符PID(process identification),类型为整型。范围为[0,32767]。而且每一个进程都有其母进程(parent process)。不过这里有一个问题:我们在不断求进程的母进程时一定会有一个终点。这个终点是唯一的吗?这个终点的母进程是谁?先放着。
3.2.2 相关系统调用
与进程有关的系统调用:
| System call | 函数原型 | 解释 |
| 创建一个子进程 | pid_t fork(); | pid_t就是int类型,无参数;创建成功,在父进程中返回子进程的pid,在子进程中返回0;失败,返回负数。(一次调用,返回两次) |
| 返回当前进程的pid | pid_t getpid(); | 返回当前进程的pid,类型为int(typedef pid_t int)。 |
| 返回当前进程的父进程的pid | pid_t getppid(); | |
| 结束进程并释放所有相关资源 | int exit(int); | 参数是什么,返回值就是什么。exit(0)表明该进程正常退出;exit(-1)或exit(1)表明程序非正常退出。 |
| 新程序执行函数。之所以叫新程序的执行,原因是这部分内容一般发生在fork()之后,在子进程中通过系统调用execve()可以将新程序加载到子进程的内存空间。这个操作会丢弃原来的子进程execve()之后的部分,而子进程的栈、数据会被新进程的相应部分所替换。即除了进程ID之外,这个进程已经与原来的进程没有关系了。 | int execve(const char *filename, char *const argv[ ], char *const envp[ ]); | 函数执行成功时没有返回值,执行失败时的返回值为-1。argv和envp是传给新程序的参数,均以NULL为最后一个元素。注意后两个参数的数据类型都是字符串数组,即char**。filename是可执行文件的路径。 |
3.2.3 代码示例,重点解释fork和execve这两个函数
#include
#include
#include
#include
//here is the program helping me learn about system calls related to PROCESS--FORK() GETPID() GETPPID()
int main(void){
int count = 0;//variable that will be duplicated to its child process
pid_t t = -1;//variable that will be duplicated to its child process
//the branch starts: child process parrallels with its parent process
t = fork();
printf("fork returned: %dn",t);
if(t < -1){
puts("fail to create a child process");
}
else if(t == 0){
puts("----------------------------");
puts("it's now in Child Process");
printf("t = %dn",t);//to verify fork() returns 0 in the child process
printf("Child Process PID: %dn",getpid());
printf("Its Parent Process PID: %dn",getppid());
count++;
printf("COUNT = %dn",count);//tp verify the child process get a copy of data from its parent
puts("----------------------------");
}
else{
puts("----------------------------");
puts("it's now in Parent Process");
printf("t = %dn",t);//to verify fork() returns child process's pid in parent prcoess
printf("Parent Process: %dn",getpid());
printf("Its Children Process: %dn",t);
printf("Its Parent Process PID: %dn",getppid());
printf("COUNT = %dn",count);
puts("----------------------------");
}
return 0;
}
对应的结果是:
问题在于子进程的父进程居然是1,这个让人不理解。先留着。不过看一下教师的ppt吧,那个例子可以。
这说明:
1.父进程执行到fork()函数时,产生进程的并行分支(该父进程与自己创建的子进程并行,父进程的变量被复制给子进程。注意,分支是在fork()处就开始了:fork以及fork之后的代码会被父子进程分别执行)。
2.fork()在父子进程中分别有返回值(在父进程中返回子进程pid,在子进程中返回0)。
3.想让父子进程分别执行不同的代码,可以利用fork不同的返回值进行if判断,如上所示。
#include#include #include #include int main(void){ char* argv[] = {"family","dad","mun","son",NULL}; char* envp[] = {0,NULL}; int t = fork(); if(t == 0){ puts("It's now in Child Process!"); execve("./processImage.out",argv,envp); puts("this line will never be performed!"); } puts("this line performed in parent process but not in child process!"); return 0; }
#includeint main(int argc,char* argv[],char* envp[]){ int i; puts("It's now in the process image running a new program!"); for(i = 0;i < argc;++i){ printf("argv[%d]=%sn",i,argv[i]); } return 0; }
结果为:
1.很明显,execv.out在子进程中调用了processImage.out,并将含有family等字符串的数组交给了它。 这个打印功能是由processImage.out提供的。
2.puts("this line will never be performed!");是子进程中execve后的代码,这个被舍弃掉了,不会执行。子进程中同样没有执行puts("this line performed in parent process but not in child process!");。这个也被舍弃了,只在父进程中被执行了一次。
3.3file descriptor文件描述符
3.3.1文件描述符和文件读写方式
在Linux OS中,一切皆文件。不光是输入输出设备(标准输入设备是键盘,标准输出设备是显示屏)被看作是文件,可以进行读写操作;就连一个套接字也可以被看作是一个文件。
在一个点对点的通信过程中,通信的两个端点各自建立一个socket,作为参与通信的进程的输入/输出缓冲区。如果向要A点向B点发送字符串"I'm here!",就需要向系统为A进程分配的输入缓冲区写入该字符串;通信过程如果没有丢包或是延迟的话,B进程的输出缓冲区就会得到这一字符串。
正如栈中的内存空间需要变量名标识,文件也需要文件描述符标识。这样我们就可以在读写操作中指定文件了。不过这里需要对文件读写方式进行说明。事实上,Linux系统和C语言库分别为我们提供了一套文件读写函数:
Linux OS为我们提供的文件读写方式是使用file descriptor来标识文件的,而C语言的读写方式是基于文件流来标识文件的。本门课程使用Linux OS提供的文件读写方式。
3.3.2 文件访问权限
在存放文件的目录下键入Linux命令ls -l,能够显示当下目录中所有文件/子目录的详细信息。在这里我们只关心前十个字符,比如“-rw-rw-r--”。这个十个字符可以被拆为两部分:第一个字符标识类型,d代表文件夹,-代表普通文件;后九个字符标识了文件的读写权限。
后九个字符可以三三三分组,分别标识u、g、o三类用户的访问权限。u表示创建该文件的用户对文件的权限,g表示创建该文件的用户所在的组的用户对该文件的权限,o表示其他人的权限。r表示读的权限,w表示写的权限,-表示执行的权限。执行的权限只对可执行文件有意义。
比如client.out这一可执行文件的权限是:user访问权限为可读可写可执行,group的执行权限为可读可写可执行,而其他人可读可写不可执行。
3.3.3 文件权限常量,文件打开方式常量,光标位置常量
在相关的系统调用前先说明这些概念,这样有助于我们更好地理解后续的系统调用。
文件权限常量
我们创建文件时,需要指定文件的权限。相应的常量是:
S_IRUSR 用户可以读 =OX(00400) S_IWUSR 用户可以写 =OX(00200) S_IXUSR 用户可以执行 =OX(00100) S_IRWXU 用户可以读、写、执行 =OX(00700) S_IRGRP 组可以读 =OX(00040) S_IWGRP 组可以写 =OX(00020) S_IXGRP 组可以执行 =OX(00010) S_IRWXG 组可以读写执行 =OX(00070) S_IROTH 其他人可以读 =OX(00004) S_IWOTH 其他人可以写 =OX(00002) S_IXOTH 其他人可以执行 =OX(00001) S_IRWXO 其他人可以读、写、执行 =OX(00007) S_ISUID //这个不用管 =OX(10000) 设置用户执行ID S_ISGID //这个不用管 =OX(01000) 设置组的执行ID
后面用五位八进制表示了该常量对应的数值(这些常量全都是int类型的)。这五位八进制数OX(abcde)的各位分别对应:a对应是否设置用户的执行ID(本课程不要求了解,设成0就好了),b对应是否设置组的执行ID(本课程不要求了解,设成0就好了),cde分别代表ugo(user,group和others)的权限。r权限被视为4(八进制的4和十进制一样),w权限被视为2,执行权限被视为1。这样,c=7表明用户同时具有rwx权限,c=6表明用户只有rw权限,c=5表明用户只有rx权限,c=4表明用户只有r权限,c=3表明用户只有wx权限,c=2表明用户只有w权限,c=1表明用户只有x权限。组和其他人也是一样。 另外由于对于二进制而言,或运算可以认为是加法运算,而且r+w+x最高为7,不会产生进位,所以10701等价于S_ISUID | S_IRWXU | S_IXOTH。
文件打开方式常量
O_RDONLY 以只读的方式打开文件 O_WRONLY 以只写的方式打开文件 O_RDWR 以读写的方式打开文件 O_APPEND 以追加的方式打开文件 O_CREAT 创建一个文件 O_TRUNC 如果文件已经存在,则删除文件的内容
我们在打开文件时,需要指定文件打开方式,以确定我们本次操作的权限——这是受限于我们对该文件拥有的权限的。比如你如果只对文件拥有rx权限,就不能在打开该文件时使用O_WRONLY。
这些也都是int常量。其中O_RDONLY、O_WEONLY和O_RDWR不可以相或。我们这门课只用的到只读、只写和读写以及创建。
光标位置常量
我们在往文件中写入内容时需要指定光标的位置。我们往文件中添加的内容是直接在光标前的,不信的话你现在打以下字,你就会发现你打出的字符出现在光标的前面。下面的这些常量全都是long型的。光标相对于文件开头的位置由光标偏移量(offset)定义。光标在文件开头处的offset为0,每多一个字符offset++。
SEEK_SET文件开头 0 SEEK_CUR当前光标位置 SEEK_END文件尾
3.3.4 与文件读写相关的系统调用
| 函数原型 | 参数返回值库 | 作用 |
| int open (char *pathname, int flag, int mode); int open(const char *pathname, int flag); | return:创建/打开文件失败返回-1,否则返回文件标识符。 parameter:pathname为文件的路径。flag指定文件的打开方式(可以使用文件的打开方式常数)。mode用于在创建文件时(flag = O_CREATE)指定文件的权限(可以使用文件权限常数)。 | 三个参数的形式用于创建文件;两个参数的形式用于打开文件。 |
| int close (int filedes); | return:0表示文件关闭成功,-1表示文件关闭失败。 parameter:filedes表示想要关闭的文件的标识符。 | 用于关闭文件 |
| int read (int filedes, char *buff, unsigned int nbytes); | reutrn:返回-1表示读取失败;成功返回nbytes即读取的字符数。 parameter:filedes表示进行读取的文件的文件标识符,buff用于存放读取内容的字符数组,nbytes本次读取的字符数。 | 用于从指定文件中读取指定长度的内容并存放到指定的字符串中 |
| int write (int filedes, char *buff, unsigned int nbytes); | reutrn:返回-1表示读取失败;成功返回nbytes即写入的字符数。 parameter:filedes表示进行写入的文件的文件标识符,buff用于存放写入内容的字符数组,nbytes本次写入的字符数。 | 用于向指定文件中写入指定长度的内容 |
| long lseek (int filedes, long offset, int whence); | return:返回光标相对于文件开头偏移量 parameter:filedes为文件标识符,offset为相对于参照点的光标偏移量(光标与参照点间的字符数量),whence为参照点,可以设置为光标位置常量。 | 用于调整光标的位置。 |
| int creat(char *pathname,int mode); | return:负数表示失败,而且不同的值对于不同的失败原因;正数表示创建成功,且为文件标识符。 parameter:参见open函数 | 创建文件 |
3.3.5 代码示例
程序一:理解open和create使用的文件权限常量。file1.out通过creat(注意没有e,不是create)和open分别创建文件file1_1.txt和file1_2.txt。希望的结果是file1_1.txt的权限为rw-r--r--,file1_2.txt的权限是r---w----。
#include#include #include #include #include #include int main(void){ //create file1_1.txt int fd1 = creat("./file1_1.txt",0644);//the highest bit 0 stands for this is in OCT if(fd1 < 0){ printf("fail to create file1_1.txtn"); } //create file1_2.txt int fd2 = open("./file1_2.txt",O_CREAT,0420); if(fd2 < 0){ printf("fail to create file1_1.txtn"); } return 0; }
程序二.写源文件file2.c,作用是往file1_1中中写入一些内容:"Los Angels FC and PhiladelphiaXXXXn"要求先写入Philadelphia Union,再写入Los Angels。
#include#include #include #include #include int main(void){ char club1[] = "Los Angeles "; char club2[] = "PhiladelphiaXXXXn"; int fd = open("file1_1.txt",O_RDWR); if(fd < -1){ printf("fail to open file1_1.txtn"); exit(1); } if(fd > 0){ write(fd,club2,sizeof(club2) - 1); lseek(fd,0,SEEK_SET); write(fd,club1,sizeof(club1) - 1); } return 0; }
这表明,O_WRONLY和O_RDWR都是覆写(清空全部内容,之后写),而不是续写(append)。第二,光标向前移动,并且键入内容时,光标不会“挤开”后续的字符,而是会直接覆写。
第三个程序为课件上的。
3.4system call系统调用
理解为Linux/Unix内核提供的函数就行。



