网络编程 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
2.2 UDP
2.3 对比
3 套接字 SOCKET
3.1 套接字概念
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两台不同的虚拟机进行通信,显然是成功的。中间我还中断了一个客户端的通信,然后我又重新启动了一次,结果服务器显示了一个新的客户端编号,即直接跳过了之前的中断了的客户端的编号但中断了的客户端。
新要求
输出学号:
Reference
UDP解释
TCP解释
套接字解释
Socket网络编程基础与实例