网络编程 pthread / fork
1.1 查看while源代码
include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#define portnumber 3333
int main(int argc, char* argv[]) {
int local_socket;
char buffer[1024];
struct sockaddr_in server_addr;
struct hostent* host;
if (argc != 2) {
fprintf(stderr, "Usage:%s hostname \a\n", argv[0]);
exit(1);
}
/* 使用hostname查询host 名字 */
if ((host = gethostbyname(argv[1])) == NULL) {
fprintf(stderr, "ERR gethostbyname\n");
exit(1);
}
/* 客户程序开始建立 local_socket描述符 */
if ((local_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // AF_INET:Internet;SOCK_STREAM:TCP
fprintf(stderr, "ERR socket:%s\a\n", strerror(errno));
exit(1);
}
/* 客户程序填充服务端的资料 */
bzero(&server_addr, sizeof(server_addr)); // 初始化,置0
server_addr.sin_family = AF_INET; // IPV4
server_addr.sin_port = htons(portnumber); // (将本机器上的short数据转化为网络上的short数据)端口号
server_addr.sin_addr = *((struct in_addr*)host->h_addr); // IP地址
/* 客户程序发起连接请求 */
if (connect(local_socket, (struct sockaddr*)(&server_addr), sizeof(struct sockaddr)) == -1) {
fprintf(stderr, "ERR connect:%s\a\n", strerror(errno));
exit(1);
}
/* 连接成功了 */
printf("Please typein a string:\n");
/* 读取和发送数据 */
fgets(buffer, 1024, stdin);
write(local_socket, buffer, strlen(buffer));
/* 结束通讯 */
close(local_socket);
exit(0);
}
2 TCP & UDP
2.1 TCP
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,TCP旨在适应支持多网络应用的分层协议层次结构。 连接到不同但互连的计算机通信网络的主计算机中的成对进程之间依靠TCP提供可靠的通信服务。TCP假设它可以从较低级别的协议获得简单的,可能不可靠的数据报服务。 原则上,TCP应该能够在从硬线连接到分组交换或电路交换网络的各种通信系统之上操作。
2.2 UDP
Internet 协议集支持一个无连接的传输协议,该协议称为用户数据报协议(UDP,User Datagram Protocol)。UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。
2.3 对比
UDP和TCP协议的主要区别是两者在如何实现信息的可靠传递方面不同。TCP协议中包含了专门的传递保证机制,当数据接收方收到发送方传来的信息时,会自动向发送方发出确认消息;发送方只有在接收到该确认消息之后才继续传送其它信息,否则将一直等待直到收到确认信息为止。与TCP不同,UDP协议并不提供数据传送的保证机制。如果在从发送方到接收方的传递过程中出现数据包的丢失,协议本身并不能做出任何检测或提示。因此,通常人们把UDP协议称为不可靠的传输协议。
TCP 是面向连接的传输控制协议,而UDP 提供了无连接的数据报服务;TCP 具有高可靠性,确保传输数据的正确性,不出现丢失或乱序;UDP 在传输数据前不建立连接,不对数据报进行检查与修改,无须等待对方的应答,所以会出现分组丢失、重复、乱序,应用程序需要负责传输可靠性方面的所有工作;UDP 具有较好的实时性,工作效率较 TCP 协议高;UDP 段结构比 TCP 的段结构简单,因此网络开销也小。TCP 协议可以保证接收端毫无差错地接收到发送端发出的字节流,为应用程序提供可靠的通信服务。对可靠性要求高的通信系统往往使用 TCP 传输数据。
3 套接字 SOCKET
3.1 套接字概念
所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。
3.2 套接字类型
套接字类型 | 作用 |
---|---|
流套接字(SOCK_STREAM) | 流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议 |
数据报套接字(SOCK_DGRAM) | 数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理 |
原始套接字(SOCK_RAW) | 原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接。 |
3.3 SOCKET相关函数、数据
3.3.1 Socket
int socket(int domain, int type, int protocol); //函数原型
此函数用于创间一个套接字(传输数据的端点)。
domain 是网络程序所在的主机所采用的通信协议族( AF_UNIX / AF_LOCAL和 AF_INET等 ,可通过man socket查看)。AF_UNIX只能够用于单一的Unix 系统进程间通信,而AF_INET是针对IPv4 Internet protocols 的,因而可以允许在远程;这里一般使用AF_INET。
type 是网络程序所采用的通讯协议(SOCK_STREAM和SOCK_DGRAM 等)。SOCK_STREAM表明所用的是TCP协议,而SOCK_DGRAM则表明所用的是UDP协议。
关于指定了protocol,因为指定了type,所以这里一般添0。
socket函数成功返回文件描述符,失败则返回 -1。
3.3.2 Bind
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); //函数原型
此函数用于将之前创建的套接字和一个地址绑定起来。(服务器使用此函数)
sockfd 为创建socket时返回的文件描述符。
addr 是sockaddr结构体变量的地址。
addrlen 是结构体sockaddr的长度。
3.3.3 Listen
int listen(int sockfd, int backlog); //函数原型
此函数用于宣告服务器可以监听连接请求。(服务器使用此函数)
sockfd 是bind后的文件描述符。
backlog 设置允许连接队列的最大长度,当队列满后新请求连接的客户端将连接不上服务器。
listen函数将bind的文件描述符变为监听套接字,返回的情况和bind一样。
3.3.4 Accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //函数原型
服务器使用此函数来确定是否接受连接请求,允许接受则建立连接,否则拒绝连接。
sockfd 是listen后的文件描述符。
addr 存放所连上客户端的相关信息(IP地址等)。
addrlen 是struct sockaddr的长度。
调用accept时,服务器端的程序会一直阻塞到有一个客户程序发出了连接.,accept成功时返回最后的服务器端的文件描述符,之后服务器端可以用该描述符和客户端程序进行通信,失败则返回 -1。
3.3.5 Connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); //函数原型
该函数是TCP用来建立连接的(客户端使用此函数)。
sockfd 是客户端调用socket函数时返回的文件描述符,用来同服务器端通讯。
addr 存放的是服务器端的连接信息(通讯协议族和端口号等)。
addrlen 是struct sockaddr的长度。
connect函数是客户端用来同服务器端建立连接的,成功是返回0,失败则返回
3.3.6 Send
ssize_t send(int sockfd, const void *buf, size_t len, int flags); //函数原型
该函数是TCP用来发送数据的。
sockfd 是发送端的套接字描述符。
buf 是要发送的数据所在的地址。
len 是实际要发送数据的长度。
flags 一般为0。
该函数成功时返回成功发送数据的字节数,失败则返回 -1。
3.3.7 Recv
ssize_t recv(int sockfd, void *buf, size_t len, int flags); //函数原型
该函数是TCP用来接收数据的。
sockfd 是接收端的套接字描述符。
buf 是存放所接收数据的地址。
len 是buf的长度。
flags 一般为0。
该函数成功时返回成功接收数据的字节数,失败则返回 -1。
3.3.8 四个结构体
struct sockaddr // 16 Bytes
struct sockaddr_in // 16 Bytes
struct sockaddr_int6 // 28 Bytes
struct sockaddr_storage // 128 Bytes
这四个数据类型都是用来存放socket信息的。
3.3.9 利用Socket方式进行数据通信与传输
- 创建服务端的socket,绑定bind建立连接的IP+端口port。
- 服务端listen、处于accept阻塞状态,等待客户机的连接。
- 创建客户端的socket,绑定bind主机名/域名/IP+端口port。
- 客户机Socket发起连接connect请求。
- 建立连接。
- 利用send/recv(sendto/recvfrom)传输数据。
- 关闭socket。
4 编程实例 (编译并在Ubuntu上运行)
client代码:
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#define portnumber 3333
int main(int argc, char* argv[]) {
int local_socket;
char buffer[1024];
struct sockaddr_in server_addr;
struct hostent* host;
if (argc != 2) {
fprintf(stderr, "Usage:%s hostname \a\n", argv[0]);
exit(1);
}
/* 使用hostname查询host 名字 */
if ((host = gethostbyname(argv[1])) == NULL) {
fprintf(stderr, "ERR gethostbyname\n");
exit(1);
}
/* 客户程序开始建立 local_socket描述符 */
if ((local_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // AF_INET:Internet;SOCK_STREAM:TCP
fprintf(stderr, "ERR socket:%s\a\n", strerror(errno));
exit(1);
}
/* 客户程序填充服务端的资料 */
bzero(&server_addr, sizeof(server_addr)); // 初始化,置0
server_addr.sin_family = AF_INET; // IPV4
server_addr.sin_port = htons(portnumber); // (将本机器上的short数据转化为网络上的short数据)端口
号
server_addr.sin_addr = *((struct in_addr*)host->h_addr); // IP地址
/* 客户程序发起连接请求 */
if (connect(local_socket, (struct sockaddr*)(&server_addr), sizeof(struct sockaddr)) == -1) {
fprintf(stderr, "ERR connect:%s\a\n", strerror(errno));
exit(1);
}
/* 连接成功了 */
printf("Please typein a string:\n");
/* 读取和发送数据 */
fgets(buffer, 1024, stdin);
write(local_socket, buffer, strlen(buffer));
/* 结束通讯 */
close(local_socket);
exit(0);
}
sever代码:
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#define portnumber 3333
int main(int argc, char* argv[]) {
int local_listen_socket, server_session_socket;
struct sockaddr_in server_addr_info_struct;
struct sockaddr_in client_addr_info_struct;
int size_of_sockaddr_in;
int read_got_bytes_nr;
char buffer[1024];
/* socket: 服务器端开始建立sockfd描述符 */
if ((local_listen_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // AF_INET i.e. IPV4; SOCK_STREAM i.e. TCP
fprintf(stderr, "Socket error:%s\n\a", strerror(errno));
exit(1);
}
/* 准备 sockaddr结构及其内部IP、端口信息 */
bzero(&server_addr_info_struct, sizeof(struct sockaddr_in)); // 初始化,置0
server_addr_info_struct.sin_family = AF_INET; // Internet
server_addr_info_struct.sin_addr.s_addr = htonl(INADDR_ANY); // 将本机host上的long数据转化为网>络上的long数据,使服务器程序能运行在不同CPU的主机上
// INADDR_ANY 表示主机监听任意/所有IP地址。
//server_addr_info_struct.sin_addr.s_addr=inet_addr("192.168.1.1"); //用于绑定到一个固定IP,inet_addr用于把数字加格式的ip转化为整形ip
server_addr_info_struct.sin_port = htons(portnumber); // (将本机器上的short数据转化为网>络上的short数据)端口号
/* bind: 绑定sockfd描述符 和 IP、端口 */
if (bind(local_listen_socket, (struct sockaddr*)(&server_addr_info_struct), sizeof(struct sockaddr)) == -1) {
fprintf(stderr, "ERR bind():%s\n\a", strerror(errno));
exit(1);
}
/* 设置允许连接的最大客户端数 */
if (listen(local_listen_socket, 5) == -1) {
fprintf(stderr, "ERR listen():%s\n\a", strerror(errno));
exit(1);
}
while (1) {
size_of_sockaddr_in = sizeof(struct sockaddr_in);
fprintf(stderr, "Listening & Accepting...\n");
if ((server_session_socket = accept(local_listen_socket, (struct sockaddr*)(&client_addr_info_struct), &size_of_sockaddr_in)) == -1) { // 服务器阻塞, 直到接受到客户连接
fprintf(stderr, "ERR accept():%s\n\a", strerror(errno));
exit(1);
}
}
fprintf(stderr, "Got connection from %s\n", inet_ntoa(client_addr_info_struct.sin_addr)); // 网络地址 转换成 字符串
if ((read_got_bytes_nr = read(server_session_socket, buffer, 1024)) == -1) {
fprintf(stderr, "ERR read():%s\n", strerror(errno));
exit(1);
}
buffer[read_got_bytes_nr] = '\0';
printf("Server received %s\n", buffer); /* 这个对话服务已经结束 */
close(server_session_socket); /* 下一个 */
}
/* 结束通讯 */
close(local_listen_socket);
exit(0);
}
5 修改服务器为多线程模式
通过将服务器改为多线程模式就可以同时间监听多个客户端的命令。
这里我采用了两台虚拟机在同一个网络下用了三个客户端对服务器进行通信,运行效果如下:
我这里用了一台Ubuntu 20.04和Ubuntu 18.04两台不同的虚拟机进行通信,显然是成功的。中间我还中断了一个客户端的通信,然后我又重新启动了一次,结果服务器显示了一个新的客户端编号,即直接跳过了之前的中断了的客户端的编号但中断了的客户端。
新要求
输出学号: