Linux C编程一站式学习读书笔记——socket编程
前言
研一的时候写过socket网络编程,研二这一年已经在用php写api都快把之前的基础知识忘干净了,这里回顾一下,主要也是项目里用到了,最近博客好杂乱啊,不过确实是到了关键时刻,各种复习加巩固准备9月份校招,顺便优美的完成手里的项目
概述
socket这个词可以有很多概念:
- 在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通迅中的一个进程,“IP地址+端口号”就称为socket
- 在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就是唯一标识一个连接。socket本身有“插座”的意思,用来描述网络连接中一对一关系
- TCP/IP协议最早是BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API
预备知识
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按照内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:
先发出去的数据是低地址,后发出去的数据是高地址
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8
为了网络程序具有可移植性,使同样的c代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
#include <arpa/inet.h> /*h表示host,n表示network, htonl表示将32位长整数从主机字节序转换为网络字节序*/ uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
socket地址的数据类型以及相关函数
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPV4,IPV6,UNIX Domain Socket
ipv4地址的数据结构 struct sockaddr_in:
基于TCP协议的网络程序
TCP协议通迅流程:
服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等到服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回
数据传输过程:
建立连接后,TCP提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去
如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭管道一样,服务器的read()返回0,这样服务器知道客户端关闭了连接,也调用close()关闭连接
简单的TCP网络程序
先写一个服务器端的监听程序,作用是从客户端读取字符,拼上自定义的字符串返回给客户端:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define MAXLINE 80 #define SERV_PORT 9933 int main(void) { struct sockaddr_in servaddr, cliaddr; // 服务器和客户端的socket数据结构 // typedef int socklen_t; // typedef int ssize_t; socklen_t cliaddr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; // 创建socket,返回整形socket标识 // domain = AF_INT : 套接字指定IPV4网络协议集 // type = SOCK_STREAM : 可靠的面向流服务器或套接字 // protocol = 0, 选择缺省的传输协议 listenfd = socket(AF_INET, SOCK_STREAM, 0); // 为服务器的sockaddr_in数据结构分配内存地址 memset(&servaddr, 0, sizeof(servaddr)); // 为服务器套接字指定协议族 servaddr.sin_family = AF_INET; // 为服务器套接字指定ip,INADDR_ANY = 0.0.0.0 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 为服务器套接字指定port servaddr.sin_port = htons(SERV_PORT); // 为一个套接字分配地址,使用socket()创建套接字后, // 只赋予其使用的协议,并未分配地址,bind()用来为listenfd // 分配地址,参数解释: // sockfd = listenfd, bind函数的套接字描述符 // my_addr = &servaddr, 服务器端sockaddr结构的指针 // addrlen = sizeof(servaddr), sockaddr结构的长度 bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); // socket监听,准备接受连接请求 listen(listenfd, 20); printf("王正一的server服务器开始等待客户端连接 ...\n"); while (1) { cliaddr_len = sizeof(cliaddr); // 三次握手完成后,服务器调用accept()接受客户端连接 // accept()参数: // sockfd = listenfd, 服务器端监听的套接字描述符 // cliaddr = &cliaddr, 指向sockaddr结构体指针,客户端 // 的地址信息 // addrlen = &cliaddr_len,确定客户端地址结构体的大小 connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); read(connfd, buf, MAXLINE); printf("从客户端%s的%d端口收到数据\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); strcat(buf, "从王正一机器返回的信息"); write(connfd, buf, MAXLINE); close(connfd); } }
运行状态:
在写一个客户端通过socket通信向服务器发送数据的代码:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define MAXLINE 80 #define SERV_PORT 9933 int main(int argc, char *argv[]) { // 需要连接的服务器端socket套接字 // 客户端的socket套接字由系统自动分配 struct sockaddr_in servaddr; char buf[MAXLINE]; int servfd; char *str; if (argc != 2) { printf("使用方法:./client 发送的字符串\n"); return 1; } str = argv[1]; servfd = socket(AF_INET, SOCK_STREAM, 0); memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = inet_addr("222.31.79.131"); // 传媒ip servaddr.sin_port = htons(SERV_PORT); // 客户端调用connect连接服务器端指定socket套接字 connect(servfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); write(servfd, str, strlen(str)); read(servfd, buf, MAXLINE); printf("从服务器返回的信息为:%s", buf); close(servfd); return 0; }
运行状态:
ok,基本完成了一个简单的TCP通信过程!!
使用fork并发处理多个client的请求
网络服务器通常用fork来同时服务多个客户端,父进程专门负责监听端口,每次accept一个新的客户端连接就fork出一个子进程专门服务这个客户端。但是子进程退出时会产生僵尸进程,父进程要注意处理SIGCHLD信号和调用wait清理僵尸进程